mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Compare commits
302 Commits
v0.5.0
...
94851a51aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
50cdd68b7b | ||
|
|
e8510b925e | ||
|
|
24e800501a |
@@ -1,5 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -7,18 +6,61 @@ REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||
exit 0
|
||||
# =============================================================================
|
||||
# Go CI checks (when core/ files are staged)
|
||||
# =============================================================================
|
||||
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
||||
|
||||
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||
echo "Go files staged in core/, running CI checks..."
|
||||
cd "$REPO_ROOT/core"
|
||||
|
||||
# Format check
|
||||
echo " Checking gofmt..."
|
||||
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
||||
if [[ -n "$UNFORMATTED" ]]; then
|
||||
echo "The following files are not formatted:"
|
||||
echo "$UNFORMATTED"
|
||||
echo ""
|
||||
echo "Run: cd core && gofmt -s -w ."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# golangci-lint
|
||||
if command -v golangci-lint &>/dev/null; then
|
||||
echo " Running golangci-lint..."
|
||||
golangci-lint run ./...
|
||||
else
|
||||
echo " Warning: golangci-lint not installed, skipping lint"
|
||||
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||
fi
|
||||
|
||||
# Tests
|
||||
echo " Running tests..."
|
||||
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
|
||||
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||
echo "Translations out of sync"
|
||||
echo "run python3 scripts/i18nsync.py sync"
|
||||
exit 1
|
||||
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
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,17 +9,9 @@ assignees: ""
|
||||
<!-- If your issue is related to ICONS
|
||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
|
||||
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||
|
||||
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
|
||||
Quickshell does not ever update its DesktopEntires.
|
||||
There is an open PR for it, that has been stuck unmerged over there to fix it.
|
||||
We unfortunately are at the mercy of quickshell to merge it.
|
||||
Until then, newly installed and removed apps will not react until the
|
||||
shell is restarted.
|
||||
-->
|
||||
|
||||
## Compositor
|
||||
|
||||
- [ ] niri
|
||||
|
||||
60
.github/workflows/go-ci.yml
vendored
Normal file
60
.github/workflows/go-ci.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Go CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'core/**'
|
||||
- '.github/workflows/go-ci.yml'
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
paths:
|
||||
- 'core/**'
|
||||
- '.github/workflows/go-ci.yml'
|
||||
|
||||
concurrency:
|
||||
group: go-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: core
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
echo "The following files are not formatted:"
|
||||
gofmt -s -l .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
working-directory: core
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build dms
|
||||
run: go build -v ./cmd/dms
|
||||
|
||||
- name: Build dms (distropkg)
|
||||
run: go build -v -tags distro_binary ./cmd/dms
|
||||
|
||||
- name: Build dankinstall
|
||||
run: go build -v ./cmd/dankinstall
|
||||
583
.github/workflows/release.yml
vendored
583
.github/workflows/release.yml
vendored
@@ -1,47 +1,194 @@
|
||||
# Release from a dispatch event from the danklinux repo
|
||||
name: Create Release from DMS
|
||||
name: Release
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [dms_release]
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.event.client_payload.tag }}
|
||||
group: release-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
create_release_from_dms:
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
TAG: ${{ github.event.client_payload.tag }}
|
||||
DMS_REPO: ${{ github.event.client_payload.dms_repo }}
|
||||
build-core:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: core
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure VERSION and tag
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
echo "${TAG}" > VERSION
|
||||
|
||||
git add -A VERSION
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "Update VERSION to ${TAG} (from DMS)"
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
echo "The following files are not formatted:"
|
||||
gofmt -s -l .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag -f "${TAG}"
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
git push origin HEAD
|
||||
git push -f origin "${TAG}"
|
||||
- name: Build dankinstall (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dankinstall
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||
-o ../../dankinstall-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Build dms (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||
-o ../../dms-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-${{ matrix.arch }}
|
||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Generate shell completions
|
||||
if: matrix.arch == 'amd64'
|
||||
run: |
|
||||
set -eux
|
||||
chmod +x dms-amd64
|
||||
./dms-amd64 completion bash > completion.bash
|
||||
./dms-amd64 completion fish > completion.fish
|
||||
./dms-amd64 completion zsh > completion.zsh
|
||||
|
||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
core/completion.bash
|
||||
core/completion.fish
|
||||
core/completion.zsh
|
||||
if-no-files-found: error
|
||||
|
||||
update-versions:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-core
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update VERSION
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
|
||||
version="${GITHUB_REF#refs/tags/}"
|
||||
echo "Updating to version: $version"
|
||||
echo "${version}" > quickshell/VERSION
|
||||
git add quickshell/VERSION
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "chore: bump version to $version"
|
||||
git pull --rebase origin master
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||
fi
|
||||
|
||||
git tag -f "${version}"
|
||||
git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [build-core, update-versions]
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch updated tag after version bump
|
||||
run: |
|
||||
git fetch origin --force tag ${{ github.ref_name }}
|
||||
git checkout ${{ github.ref_name }}
|
||||
|
||||
- name: Download core artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: core-assets-*
|
||||
merge-multiple: true
|
||||
path: ./_core_assets
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
@@ -55,6 +202,12 @@ jobs:
|
||||
fi
|
||||
|
||||
cat > RELEASE_BODY.md << 'EOF'
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.danklinux.com | sh
|
||||
```
|
||||
|
||||
## Assets
|
||||
|
||||
### Complete Packages
|
||||
@@ -66,6 +219,8 @@ jobs:
|
||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||
- **`dms-qml.tar.gz`** - QML source code only
|
||||
|
||||
### Checksums
|
||||
@@ -89,30 +244,14 @@ jobs:
|
||||
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create/Update DankMaterialShell Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
name: Release ${{ env.TAG }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
draft: false
|
||||
prerelease: ${{ contains(env.TAG, '-') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download and prepare release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
mkdir -p _release_assets
|
||||
|
||||
# Download DMS CLI binaries from the danklinux repo
|
||||
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
|
||||
|
||||
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
|
||||
for file in _dms_assets/dms-*.gz*; do
|
||||
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
||||
for file in _core_assets/dms-*.gz*; do
|
||||
if [ -f "$file" ]; then
|
||||
basename=$(basename "$file")
|
||||
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||
@@ -124,40 +263,59 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
# Create QML source package (exclude .git, .github, build artifacts)
|
||||
tar --exclude='.git' \
|
||||
# Copy dankinstall binaries
|
||||
cp _core_assets/dankinstall-*.gz* _release_assets/
|
||||
|
||||
# Copy completions
|
||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||
|
||||
# Create QML source package (exclude build artifacts and git files)
|
||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||
cp LICENSE CONTRIBUTING.md quickshell/
|
||||
|
||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||
(cd quickshell && tar --exclude='.git' \
|
||||
--exclude='.github' \
|
||||
--exclude='_dms_assets' \
|
||||
--exclude='_release_assets' \
|
||||
--exclude='*.tar.gz' \
|
||||
-czf _release_assets/dms-qml.tar.gz .
|
||||
-czf ../_release_assets/dms-qml.tar.gz .)
|
||||
|
||||
# Generate checksum for QML package
|
||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||
|
||||
# Create full packages for each architecture
|
||||
for arch in amd64 arm64; do
|
||||
mkdir -p _temp_full/dms
|
||||
mkdir -p _temp_full/bin
|
||||
mkdir -p _temp_full/completions
|
||||
|
||||
# Extract QML source
|
||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||
|
||||
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||
# Add CLI binaries
|
||||
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
||||
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||
chmod +x _temp_full/bin/dms
|
||||
fi
|
||||
|
||||
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
||||
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||
chmod +x _temp_full/bin/dms-distropkg
|
||||
fi
|
||||
|
||||
for completion in _dms_assets/completion.*; do
|
||||
# Add shell completions
|
||||
for completion in _core_assets/completion.*; do
|
||||
if [ -f "$completion" ]; then
|
||||
cp "$completion" _temp_full/completions/
|
||||
fi
|
||||
done
|
||||
cat > _temp_full/INSTALL.md << 'EOF'
|
||||
|
||||
# Copy docs directory
|
||||
if [ -d "docs" ]; then
|
||||
cp -r docs _temp_full/
|
||||
fi
|
||||
|
||||
# Create installation guide
|
||||
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
||||
# DankMaterialShell Installation
|
||||
|
||||
## Requirements
|
||||
@@ -204,10 +362,9 @@ jobs:
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
|
||||
- Check logs in `~/.local/state/DankMaterialShell/`
|
||||
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
|
||||
- Ensure all dependencies are installed
|
||||
EOF
|
||||
EOFINSTALL
|
||||
|
||||
# Create the full package
|
||||
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
||||
@@ -219,11 +376,319 @@ jobs:
|
||||
rm -rf _temp_full
|
||||
done
|
||||
|
||||
- name: Attach all assets to release
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
name: Release ${{ env.TAG }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
files: _release_assets/**
|
||||
draft: false
|
||||
prerelease: ${{ contains(env.TAG, '-') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
trigger-obs-update:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install OSC
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
|
||||
- name: Update OBS packages
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
cd distro
|
||||
bash scripts/obs-upload.sh dms "Update to $VERSION"
|
||||
|
||||
trigger-ppa-update:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
debhelper \
|
||||
devscripts \
|
||||
dput \
|
||||
lftp \
|
||||
build-essential \
|
||||
fakeroot \
|
||||
dpkg-dev
|
||||
|
||||
- name: Configure GPG
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_KEY" | gpg --import
|
||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload to PPA
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
cd distro/ubuntu/ppa
|
||||
bash create-and-upload.sh ../dms dms questing
|
||||
|
||||
copr-build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${TAG#v}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building DMS stable version: $VERSION"
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm wget curl jq gzip
|
||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
cd ~/rpmbuild/SOURCES
|
||||
|
||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Generate stable spec file
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||
|
||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
||||
|
||||
%global debug_package %{nil}
|
||||
%global version VERSION_PLACEHOLDER
|
||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||
|
||||
Name: dms
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: %{pkg_summary}
|
||||
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
Source0: dms-qml.tar.gz
|
||||
|
||||
BuildRequires: gzip
|
||||
BuildRequires: wget
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
Requires: (quickshell or quickshell-git)
|
||||
Requires: accountsservice
|
||||
Requires: dms-cli
|
||||
Requires: dgop
|
||||
|
||||
Recommends: cava
|
||||
Recommends: cliphist
|
||||
Recommends: danksearch
|
||||
Recommends: hyprpicker
|
||||
Recommends: matugen
|
||||
Recommends: wl-clipboard
|
||||
Recommends: NetworkManager
|
||||
Recommends: qt6-qtmultimedia
|
||||
Suggests: qt6ct
|
||||
|
||||
%description
|
||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||
and optimized for the niri and hyprland compositors. Features notifications,
|
||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
||||
|
||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||
process monitoring, notification center, clipboard history, dock, control center,
|
||||
lock screen, and comprehensive plugin system.
|
||||
|
||||
%package -n dms-cli
|
||||
Summary: DankMaterialShell CLI tool
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
%description -n dms-cli
|
||||
Command-line interface for DankMaterialShell configuration and management.
|
||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||
|
||||
%package -n dgop
|
||||
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/dgop
|
||||
Provides: dgop
|
||||
|
||||
%description -n dgop
|
||||
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
||||
network statistics. Designed for integration with DankMaterialShell but can be
|
||||
used standalone. This package always includes the latest stable dgop release.
|
||||
|
||||
%prep
|
||||
%setup -q -c -n dms-qml
|
||||
|
||||
# Download architecture-specific binaries during build
|
||||
case "%{_arch}" in
|
||||
x86_64)
|
||||
ARCH_SUFFIX="amd64"
|
||||
;;
|
||||
aarch64)
|
||||
ARCH_SUFFIX="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: %{_arch}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||
exit 1
|
||||
}
|
||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||
chmod +x %{_builddir}/dms-cli
|
||||
|
||||
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
||||
echo "Failed to download dgop for architecture %{_arch}"
|
||||
exit 1
|
||||
}
|
||||
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
||||
chmod +x %{_builddir}/dgop
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||
|
||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||
|
||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||
|
||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||
|
||||
%posttrans
|
||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$1" -ge 2 ]; then
|
||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md CONTRIBUTING.md
|
||||
%{_datadir}/quickshell/dms/
|
||||
%{_userunitdir}/dms.service
|
||||
|
||||
%files -n dms-cli
|
||||
%{_bindir}/dms
|
||||
%{_datadir}/bash-completion/completions/dms
|
||||
%{_datadir}/zsh/site-functions/_dms
|
||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||
|
||||
%files -n dgop
|
||||
%{_bindir}/dgop
|
||||
|
||||
%changelog
|
||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
||||
- Stable release VERSION_PLACEHOLDER
|
||||
- Built from GitHub release
|
||||
- Includes latest dms-cli and dgop binaries
|
||||
SPECEOF
|
||||
|
||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
|
||||
- name: Build SRPM
|
||||
id: build
|
||||
run: |
|
||||
cd ~/rpmbuild/SPECS
|
||||
rpmbuild -bs dms.spec
|
||||
|
||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||
SRPM_NAME=$(basename "$SRPM")
|
||||
|
||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||
echo "SRPM built: $SRPM_NAME"
|
||||
|
||||
- name: Upload SRPM artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||
path: ${{ steps.build.outputs.srpm_path }}
|
||||
retention-days: 90
|
||||
|
||||
- name: Install Copr CLI
|
||||
run: |
|
||||
sudo apt-get install -y python3-pip
|
||||
pip3 install copr-cli
|
||||
|
||||
mkdir -p ~/.config
|
||||
cat > ~/.config/copr << EOF
|
||||
[copr-cli]
|
||||
login = ${{ secrets.COPR_LOGIN }}
|
||||
username = avengemedia
|
||||
token = ${{ secrets.COPR_TOKEN }}
|
||||
copr_url = https://copr.fedorainfracloud.org
|
||||
EOF
|
||||
chmod 600 ~/.config/copr
|
||||
|
||||
- name: Upload to Copr
|
||||
run: |
|
||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
echo "Uploading SRPM to avengemedia/dms..."
|
||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||
echo "$BUILD_OUTPUT"
|
||||
|
||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||
|
||||
if [ "$BUILD_ID" != "unknown" ]; then
|
||||
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||
fi
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
name: DMS Copr Stable Release
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Create Release from DMS"]
|
||||
types: [completed]
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||
required: false
|
||||
default: ''
|
||||
release:
|
||||
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||
required: false
|
||||
default: '1'
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -24,19 +23,23 @@ jobs:
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
# Get version from manual input or latest release
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
echo "Using manual version: $VERSION"
|
||||
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
|
||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
echo "Using latest release version from workflow_run: $VERSION"
|
||||
else
|
||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
echo "Using latest release version: $VERSION"
|
||||
fi
|
||||
|
||||
RELEASE="${{ github.event.inputs.release }}"
|
||||
if [ -z "$RELEASE" ]; then
|
||||
RELEASE="1"
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "✅ Building DMS stable version: $VERSION"
|
||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
@@ -65,6 +68,7 @@ jobs:
|
||||
- name: Generate stable spec file
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE="${{ steps.version.outputs.release }}"
|
||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||
|
||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||
@@ -76,7 +80,7 @@ jobs:
|
||||
|
||||
Name: dms
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Release: RELEASE_PLACEHOLDER%{?dist}
|
||||
Summary: %{pkg_summary}
|
||||
|
||||
License: MIT
|
||||
@@ -115,7 +119,7 @@ jobs:
|
||||
%package -n dms-cli
|
||||
Summary: DankMaterialShell CLI tool
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/danklinux
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
%description -n dms-cli
|
||||
Command-line interface for DankMaterialShell configuration and management.
|
||||
@@ -151,7 +155,7 @@ jobs:
|
||||
esac
|
||||
|
||||
# Download dms-cli for target architecture
|
||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||
exit 1
|
||||
}
|
||||
@@ -188,7 +192,7 @@ jobs:
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/*.spec
|
||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||
|
||||
%posttrans
|
||||
# Clean up old installation path from previous versions (only if empty)
|
||||
@@ -220,16 +224,17 @@ jobs:
|
||||
%{_bindir}/dgop
|
||||
|
||||
%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
|
||||
- Built from GitHub release
|
||||
- Includes latest dms-cli and dgop binaries
|
||||
SPECEOF
|
||||
|
||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||
|
||||
echo "✅ Spec file generated for v${VERSION}"
|
||||
|
||||
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
||||
echo ""
|
||||
echo "=== Spec file preview ==="
|
||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
||||
@@ -303,7 +308,7 @@ jobs:
|
||||
run: |
|
||||
echo "### 🎉 DMS Stable Build Summary" >> $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 "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
243
.github/workflows/run-obs.yml
vendored
Normal file
243
.github/workflows/run-obs.yml
vendored
Normal file
@@ -0,0 +1,243 @@
|
||||
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 latest commit hash from master branch
|
||||
LATEST_COMMIT=$(git rev-parse origin/master 2>/dev/null || git rev-parse master 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$LATEST_COMMIT" ]]; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Could not determine git commit, proceeding with update"
|
||||
else
|
||||
# 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
|
||||
|
||||
# Check tarball age - if older than 3 hours, update needed
|
||||
if [[ -f "dms-git-source.tar.gz" ]]; then
|
||||
TARBALL_MTIME=$(stat -c%Y "dms-git-source.tar.gz" 2>/dev/null || echo "0")
|
||||
CURRENT_TIME=$(date +%s)
|
||||
AGE_SECONDS=$((CURRENT_TIME - TARBALL_MTIME))
|
||||
AGE_HOURS=$((AGE_SECONDS / 3600))
|
||||
|
||||
# If tarball is older than 3 hours, check for new commits
|
||||
if [[ $AGE_HOURS -ge 3 ]]; then
|
||||
# Check if there are new commits in the last 3 hours
|
||||
cd "${{ github.workspace }}"
|
||||
NEW_COMMITS=$(git log --since="3 hours ago" --oneline origin/master 2>/dev/null | wc -l)
|
||||
|
||||
if [[ $NEW_COMMITS -gt 0 ]]; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 New commits detected in last 3 hours, update needed"
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 No new commits in last 3 hours, skipping update"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "📋 Recent upload exists (< 3 hours), skipping update"
|
||||
fi
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 No existing tarball in OBS, update needed"
|
||||
fi
|
||||
cd "${{ github.workspace }}"
|
||||
else
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "📋 First upload to OBS, update needed"
|
||||
fi
|
||||
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 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
|
||||
123
.github/workflows/run-ppa.yml
vendored
Normal file
123
.github/workflows/run-ppa.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
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:
|
||||
upload-ppa:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: uploading git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
else
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload to PPA
|
||||
env:
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
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
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
if [[ "$PACKAGES" == "all" ]]; then
|
||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
66
.github/workflows/update-vendor-hash.yml
vendored
Normal file
66
.github/workflows/update-vendor-hash.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Update Vendor Hash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "core/go.mod"
|
||||
- "core/go.sum"
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-vendor-hash:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Update vendorHash in flake.nix
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Attempting nix build to get new vendorHash..."
|
||||
if output=$(nix build .#dmsCli 2>&1); then
|
||||
echo "Build succeeded, no hash update needed"
|
||||
exit 0
|
||||
fi
|
||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||
echo "Verifying build with new vendorHash..."
|
||||
nix build .#dmsCli
|
||||
echo "vendorHash updated successfully!"
|
||||
|
||||
- name: Commit and push vendorHash update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! git diff --quiet flake.nix; then
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add flake.nix
|
||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||
git pull --rebase origin master
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||
else
|
||||
echo "No changes to flake.nix"
|
||||
fi
|
||||
44
.gitignore
vendored
44
.gitignore
vendored
@@ -27,7 +27,6 @@ qrc_*.cpp
|
||||
ui_*.h
|
||||
*.qmlc
|
||||
*.jsc
|
||||
Makefile*
|
||||
*build-*
|
||||
*.qm
|
||||
*.prl
|
||||
@@ -101,4 +100,45 @@ go.work.sum
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
# .vscode/
|
||||
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
|
||||
bin/
|
||||
|
||||
# Extracted source trees in Ubuntu package directories
|
||||
distro/ubuntu/*/dms-git-repo/
|
||||
distro/ubuntu/*/DankMaterialShell-*/
|
||||
distro/ubuntu/danklinux/*/dsearch-*/
|
||||
distro/ubuntu/danklinux/*/dgop-*/
|
||||
|
||||
@@ -2,28 +2,50 @@
|
||||
|
||||
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.
|
||||
|
||||
```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
|
||||
},
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtCore
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property url home: StandardPaths.standardLocations(
|
||||
StandardPaths.HomeLocation)[0]
|
||||
readonly property url pictures: StandardPaths.standardLocations(
|
||||
StandardPaths.PicturesLocation)[0]
|
||||
|
||||
readonly property url data: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||
readonly property url state: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||
readonly property url cache: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
|
||||
readonly property url config: `${StandardPaths.standardLocations(
|
||||
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
|
||||
|
||||
readonly property url imagecache: `${cache}/imagecache`
|
||||
|
||||
function stringify(path: url): string {
|
||||
return path.toString().replace(/%20/g, " ")
|
||||
}
|
||||
|
||||
function expandTilde(path: string): string {
|
||||
return strip(path.replace("~", stringify(root.home)))
|
||||
}
|
||||
|
||||
function shortenHome(path: string): string {
|
||||
return path.replace(strip(root.home), "~")
|
||||
}
|
||||
|
||||
function strip(path: url): string {
|
||||
return stringify(path).replace("file://", "")
|
||||
}
|
||||
|
||||
function toFileUrl(path: string): string {
|
||||
return path.startsWith("file://") ? path : "file://" + path
|
||||
}
|
||||
|
||||
function mkdir(path: url): void {
|
||||
Quickshell.execDetached(["mkdir", "-p", strip(path)])
|
||||
}
|
||||
|
||||
function copy(from: url, to: url): void {
|
||||
Quickshell.execDetached(["cp", strip(from), strip(to)])
|
||||
}
|
||||
|
||||
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
||||
function moddedAppId(appId: string): string {
|
||||
if (appId === "Spotify")
|
||||
return "spotify"
|
||||
if (appId === "beepertexts")
|
||||
return "beeper"
|
||||
if (appId === "home assistant desktop")
|
||||
return "homeassistant-desktop"
|
||||
if (appId.includes("com.transmissionbt.transmission"))
|
||||
return "transmission-gtk"
|
||||
return appId
|
||||
}
|
||||
}
|
||||
@@ -1,943 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Common.settings
|
||||
import qs.Services
|
||||
import "settings/SettingsSpec.js" as Spec
|
||||
import "settings/SettingsStore.js" as Store
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int settingsConfigVersion: 1
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
enum Position {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
enum AnimationSpeed {
|
||||
None,
|
||||
Short,
|
||||
Medium,
|
||||
Long,
|
||||
Custom
|
||||
}
|
||||
|
||||
enum SuspendBehavior {
|
||||
Suspend,
|
||||
Hibernate,
|
||||
SuspendThenHibernate
|
||||
}
|
||||
|
||||
readonly property string defaultFontFamily: "Inter Variable"
|
||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
||||
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
readonly property string _configDir: Paths.strip(_configUrl)
|
||||
readonly property string pluginSettingsPath: _configDir + "/DankMaterialShell/plugin_settings.json"
|
||||
|
||||
property bool _loading: false
|
||||
property bool _pluginSettingsLoading: false
|
||||
property bool hasTriedDefaultSettings: false
|
||||
property var pluginSettings: ({})
|
||||
|
||||
property alias dankBarLeftWidgetsModel: leftWidgetsModel
|
||||
property alias dankBarCenterWidgetsModel: centerWidgetsModel
|
||||
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
||||
|
||||
property string currentThemeName: "blue"
|
||||
property string customThemeFile: ""
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
property bool runUserMatugenTemplates: true
|
||||
property string matugenTargetMonitor: ""
|
||||
property real dankBarTransparency: 1.0
|
||||
property real dankBarWidgetTransparency: 1.0
|
||||
property real popupTransparency: 1.0
|
||||
property real dockTransparency: 1
|
||||
property string widgetBackgroundColor: "sch"
|
||||
property real cornerRadius: 12
|
||||
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool useFahrenheit: false
|
||||
property bool nightModeEnabled: false
|
||||
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
||||
property int customAnimationDuration: 500
|
||||
property string wallpaperFillMode: "Fill"
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
|
||||
property bool showLauncherButton: true
|
||||
property bool showWorkspaceSwitcher: true
|
||||
property bool showFocusedWindow: true
|
||||
property bool showWeather: true
|
||||
property bool showMusic: true
|
||||
property bool showClipboard: true
|
||||
property bool showCpuUsage: true
|
||||
property bool showMemUsage: true
|
||||
property bool showCpuTemp: true
|
||||
property bool showGpuTemp: true
|
||||
property int selectedGpuIndex: 0
|
||||
property var enabledGpuPciIds: []
|
||||
property bool showSystemTray: true
|
||||
property bool showClock: true
|
||||
property bool showNotificationButton: true
|
||||
property bool showBattery: true
|
||||
property bool showControlCenterButton: true
|
||||
|
||||
property bool controlCenterShowNetworkIcon: true
|
||||
property bool controlCenterShowBluetoothIcon: true
|
||||
property bool controlCenterShowAudioIcon: true
|
||||
property var controlCenterWidgets: [{
|
||||
"id": "volumeSlider",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "brightnessSlider",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "wifi",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "bluetooth",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "audioOutput",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "audioInput",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "nightMode",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}, {
|
||||
"id": "darkMode",
|
||||
"enabled": true,
|
||||
"width": 50
|
||||
}]
|
||||
|
||||
property bool showWorkspaceIndex: false
|
||||
property bool showWorkspacePadding: false
|
||||
property bool workspaceScrolling: false
|
||||
property bool showWorkspaceApps: false
|
||||
property int maxWorkspaceIcons: 3
|
||||
property bool workspacesPerMonitor: true
|
||||
property bool dwlShowAllTags: false
|
||||
property var workspaceNameIcons: ({})
|
||||
property bool waveProgressEnabled: true
|
||||
property bool clockCompactMode: false
|
||||
property bool focusedWindowCompactMode: false
|
||||
property bool runningAppsCompactMode: true
|
||||
property bool keyboardLayoutNameCompactMode: false
|
||||
property bool runningAppsCurrentWorkspace: false
|
||||
property bool runningAppsGroupByApp: false
|
||||
property string clockDateFormat: ""
|
||||
property string lockDateFormat: ""
|
||||
property int mediaSize: 1
|
||||
|
||||
property var dankBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||
property var dankBarCenterWidgets: ["music", "clock", "weather"]
|
||||
property var dankBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
||||
property var dankBarWidgetOrder: []
|
||||
|
||||
property string appLauncherViewMode: "list"
|
||||
property string spotlightModalViewMode: "list"
|
||||
property bool sortAppsAlphabetically: false
|
||||
|
||||
property string weatherLocation: "New York, NY"
|
||||
property string weatherCoordinates: "40.7128,-74.0060"
|
||||
property bool useAutoLocation: false
|
||||
property bool weatherEnabled: true
|
||||
|
||||
property string networkPreference: "auto"
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
property string iconTheme: "System Default"
|
||||
property var availableIconThemes: ["System Default"]
|
||||
property string systemDefaultIconTheme: ""
|
||||
property bool qt5ctAvailable: false
|
||||
property bool qt6ctAvailable: false
|
||||
property bool gtkAvailable: false
|
||||
|
||||
property string launcherLogoMode: "apps"
|
||||
property string launcherLogoCustomPath: ""
|
||||
property string launcherLogoColorOverride: ""
|
||||
property bool launcherLogoColorInvertOnMode: false
|
||||
property real launcherLogoBrightness: 0.5
|
||||
property real launcherLogoContrast: 1
|
||||
property int launcherLogoSizeOffset: 0
|
||||
|
||||
property string fontFamily: "Inter Variable"
|
||||
property string monoFontFamily: "Fira Code"
|
||||
property int fontWeight: Font.Normal
|
||||
property real fontScale: 1.0
|
||||
property real dankBarFontScale: 1.0
|
||||
|
||||
property bool notepadUseMonospace: true
|
||||
property string notepadFontFamily: ""
|
||||
property real notepadFontSize: 14
|
||||
property bool notepadShowLineNumbers: false
|
||||
property real notepadTransparencyOverride: -1
|
||||
property real notepadLastCustomTransparency: 0.7
|
||||
|
||||
onNotepadUseMonospaceChanged: saveSettings()
|
||||
onNotepadFontFamilyChanged: saveSettings()
|
||||
onNotepadFontSizeChanged: saveSettings()
|
||||
onNotepadShowLineNumbersChanged: saveSettings()
|
||||
onNotepadTransparencyOverrideChanged: {
|
||||
if (notepadTransparencyOverride > 0) {
|
||||
notepadLastCustomTransparency = notepadTransparencyOverride
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
onNotepadLastCustomTransparencyChanged: saveSettings()
|
||||
|
||||
property bool soundsEnabled: true
|
||||
property bool useSystemSoundTheme: false
|
||||
property bool soundNewNotification: true
|
||||
property bool soundVolumeChanged: true
|
||||
property bool soundPluggedIn: true
|
||||
|
||||
property int acMonitorTimeout: 0
|
||||
property int acLockTimeout: 0
|
||||
property int acSuspendTimeout: 0
|
||||
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||
property int batteryMonitorTimeout: 0
|
||||
property int batteryLockTimeout: 0
|
||||
property int batterySuspendTimeout: 0
|
||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||
property bool lockBeforeSuspend: false
|
||||
property bool preventIdleForMedia: false
|
||||
property bool loginctlLockIntegration: true
|
||||
property string launchPrefix: ""
|
||||
property var brightnessDevicePins: ({})
|
||||
|
||||
property bool gtkThemingEnabled: false
|
||||
property bool qtThemingEnabled: false
|
||||
property bool syncModeWithPortal: true
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property bool dockOpenOnOverview: false
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
property real dockSpacing: 4
|
||||
property real dockBottomGap: 0
|
||||
property real dockMargin: 0
|
||||
property real dockIconSize: 40
|
||||
property string dockIndicatorStyle: "circle"
|
||||
|
||||
property bool notificationOverlayEnabled: false
|
||||
property bool dankBarAutoHide: false
|
||||
property bool dankBarOpenOnOverview: false
|
||||
property bool dankBarVisible: true
|
||||
property int overviewRows: 2
|
||||
property int overviewColumns: 5
|
||||
property real overviewScale: 0.16
|
||||
property real dankBarSpacing: 4
|
||||
property real dankBarBottomGap: 0
|
||||
property real dankBarInnerPadding: 4
|
||||
property int dankBarPosition: SettingsData.Position.Top
|
||||
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
property bool dankBarSquareCorners: false
|
||||
property bool dankBarNoBackground: false
|
||||
property bool dankBarGothCornersEnabled: false
|
||||
property bool dankBarGothCornerRadiusOverride: false
|
||||
property real dankBarGothCornerRadiusValue: 12
|
||||
property bool dankBarBorderEnabled: false
|
||||
property string dankBarBorderColor: "surfaceText"
|
||||
property real dankBarBorderOpacity: 1.0
|
||||
property real dankBarBorderThickness: 1
|
||||
|
||||
onDankBarGothCornerRadiusOverrideChanged: saveSettings()
|
||||
onDankBarGothCornerRadiusValueChanged: saveSettings()
|
||||
onDankBarBorderColorChanged: saveSettings()
|
||||
onDankBarBorderOpacityChanged: saveSettings()
|
||||
onDankBarBorderThicknessChanged: saveSettings()
|
||||
|
||||
property bool popupGapsAuto: true
|
||||
property int popupGapsManual: 4
|
||||
|
||||
property bool modalDarkenBackground: true
|
||||
|
||||
property bool lockScreenShowPowerActions: true
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 3
|
||||
property bool fprintdAvailable: false
|
||||
property bool hideBrightnessSlider: false
|
||||
|
||||
property int notificationTimeoutLow: 5000
|
||||
property int notificationTimeoutNormal: 5000
|
||||
property int notificationTimeoutCritical: 0
|
||||
property int notificationPopupPosition: SettingsData.Position.Top
|
||||
|
||||
property bool osdAlwaysShowValue: false
|
||||
|
||||
property bool powerActionConfirm: true
|
||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
property string powerMenuDefaultAction: "logout"
|
||||
property string customPowerActionLock: ""
|
||||
property string customPowerActionLogout: ""
|
||||
property string customPowerActionSuspend: ""
|
||||
property string customPowerActionHibernate: ""
|
||||
property string customPowerActionReboot: ""
|
||||
property string customPowerActionPowerOff: ""
|
||||
|
||||
property bool updaterUseCustomCommand: false
|
||||
property string updaterCustomCommand: ""
|
||||
property string updaterTerminalAdditionalParams: ""
|
||||
|
||||
property var screenPreferences: ({})
|
||||
property var showOnLastDisplay: ({})
|
||||
|
||||
signal forceDankBarLayoutRefresh
|
||||
signal forceDockLayoutRefresh
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
Processes.settingsRoot = root
|
||||
loadSettings()
|
||||
initializeListModels()
|
||||
Processes.detectFprintd()
|
||||
Processes.checkPluginSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoredTheme() {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.switchTheme(currentThemeName, false, false)
|
||||
} else {
|
||||
Qt.callLater(function() {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.switchTheme(currentThemeName, false, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function regenSystemThemes() {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function updateNiriLayout() {
|
||||
if (typeof NiriService !== "undefined" && typeof CompositorService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoredIconTheme() {
|
||||
updateGtkIconTheme()
|
||||
updateQtIconTheme()
|
||||
}
|
||||
|
||||
function updateGtkIconTheme() {
|
||||
const gtkThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme
|
||||
if (gtkThemeName === "System Default" || gtkThemeName === "") return
|
||||
|
||||
if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") {
|
||||
PortalService.setSystemIconTheme(gtkThemeName)
|
||||
}
|
||||
|
||||
const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0
|
||||
|
||||
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
|
||||
settings_file="$config_dir/settings.ini"
|
||||
if [ -f "$settings_file" ]; then
|
||||
if grep -q "^gtk-icon-theme-name=" "$settings_file"; then
|
||||
sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file"
|
||||
else
|
||||
if grep -q "\\[Settings\\]" "$settings_file"; then
|
||||
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
|
||||
else
|
||||
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
|
||||
pkill -HUP -f 'gtk' 2>/dev/null || true`
|
||||
|
||||
Quickshell.execDetached(["sh", "-lc", configScript])
|
||||
}
|
||||
|
||||
function updateQtIconTheme() {
|
||||
const qtThemeName = (iconTheme === "System Default") ? "" : iconTheme
|
||||
if (!qtThemeName) return
|
||||
|
||||
const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''")
|
||||
const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''")
|
||||
|
||||
const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true
|
||||
update_qt_icon_theme() {
|
||||
local config_file="$1"
|
||||
local theme_name="$2"
|
||||
if [ -f "$config_file" ]; then
|
||||
if grep -q "^\\[Appearance\\]" "$config_file"; then
|
||||
if grep -q "^icon_theme=" "$config_file"; then
|
||||
sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file"
|
||||
else
|
||||
sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file"
|
||||
fi
|
||||
else
|
||||
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
|
||||
fi
|
||||
else
|
||||
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
|
||||
fi
|
||||
}
|
||||
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
|
||||
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
|
||||
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`
|
||||
|
||||
Quickshell.execDetached(["sh", "-lc", script])
|
||||
}
|
||||
|
||||
readonly property var _hooks: ({
|
||||
applyStoredTheme: applyStoredTheme,
|
||||
regenSystemThemes: regenSystemThemes,
|
||||
updateNiriLayout: updateNiriLayout,
|
||||
applyStoredIconTheme: applyStoredIconTheme
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
Spec.set(root, key, value, saveSettings, _hooks)
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
_loading = true
|
||||
try {
|
||||
const txt = settingsFile.text()
|
||||
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
|
||||
Store.parse(root, obj)
|
||||
const shouldMigrate = Store.migrate(root, obj)
|
||||
applyStoredTheme()
|
||||
applyStoredIconTheme()
|
||||
Processes.detectIcons()
|
||||
Processes.detectQtTools()
|
||||
if (obj && obj.configVersion === undefined) {
|
||||
const cleaned = Store.cleanup(txt)
|
||||
if (cleaned) {
|
||||
settingsFile.setText(cleaned)
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
if (shouldMigrate) {
|
||||
savePluginSettings()
|
||||
saveSettings()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to load settings:", e.message)
|
||||
applyStoredTheme()
|
||||
applyStoredIconTheme()
|
||||
} finally {
|
||||
_loading = false
|
||||
}
|
||||
loadPluginSettings()
|
||||
}
|
||||
|
||||
function loadPluginSettings() {
|
||||
_pluginSettingsLoading = true
|
||||
parsePluginSettings(pluginSettingsFile.text())
|
||||
_pluginSettingsLoading = false
|
||||
}
|
||||
|
||||
function parsePluginSettings(content) {
|
||||
_pluginSettingsLoading = true
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
pluginSettings = JSON.parse(content)
|
||||
} else {
|
||||
pluginSettings = {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to parse plugin settings:", e.message)
|
||||
pluginSettings = {}
|
||||
} finally {
|
||||
_pluginSettingsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (_loading) return
|
||||
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2))
|
||||
}
|
||||
|
||||
function savePluginSettings() {
|
||||
if (_pluginSettingsLoading) return
|
||||
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2))
|
||||
}
|
||||
|
||||
function detectAvailableIconThemes() {
|
||||
Processes.detectIcons()
|
||||
}
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock) {
|
||||
return showSeconds ? "hh:mm:ss" : "hh:mm"
|
||||
} else {
|
||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP"
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveClockDateFormat() {
|
||||
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d"
|
||||
}
|
||||
|
||||
function getEffectiveLockDateFormat() {
|
||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
|
||||
}
|
||||
|
||||
function initializeListModels() {
|
||||
Lists.init(leftWidgetsModel, centerWidgetsModel, rightWidgetsModel, dankBarLeftWidgets, dankBarCenterWidgets, dankBarRightWidgets)
|
||||
}
|
||||
|
||||
function updateListModel(listModel, order) {
|
||||
Lists.update(listModel, order)
|
||||
widgetDataChanged()
|
||||
}
|
||||
|
||||
function hasNamedWorkspaces() {
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return false
|
||||
|
||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
var ws = NiriService.allWorkspaces[i]
|
||||
if (ws.name && ws.name.trim() !== "") return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getNamedWorkspaces() {
|
||||
var namedWorkspaces = []
|
||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return namedWorkspaces
|
||||
|
||||
for (const ws of NiriService.allWorkspaces) {
|
||||
if (ws.name && ws.name.trim() !== "") {
|
||||
namedWorkspaces.push(ws.name)
|
||||
}
|
||||
}
|
||||
return namedWorkspaces
|
||||
}
|
||||
|
||||
function getPopupYPosition(barHeight) {
|
||||
const gothOffset = dankBarGothCornersEnabled ? Theme.cornerRadius : 0
|
||||
return barHeight + dankBarSpacing + dankBarBottomGap - gothOffset + Theme.popupDistance
|
||||
}
|
||||
|
||||
function getPopupTriggerPosition(globalPos, screen, barThickness, widgetWidth) {
|
||||
const screenX = screen ? screen.x : 0
|
||||
const screenY = screen ? screen.y : 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const relativeY = globalPos.y - screenY
|
||||
|
||||
if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) {
|
||||
return {
|
||||
"x": relativeY,
|
||||
"y": barThickness + dankBarSpacing + Theme.popupDistance,
|
||||
"width": widgetWidth
|
||||
}
|
||||
}
|
||||
return {
|
||||
"x": relativeX,
|
||||
"y": barThickness + dankBarSpacing + Theme.popupDistance,
|
||||
"width": widgetWidth
|
||||
}
|
||||
}
|
||||
|
||||
function getFilteredScreens(componentId) {
|
||||
var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
|
||||
if (prefs.includes("all")) {
|
||||
return Quickshell.screens
|
||||
}
|
||||
var filtered = Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||
if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) {
|
||||
return Quickshell.screens
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
function sendTestNotifications() {
|
||||
sendTestNotification(0)
|
||||
testNotifTimer1.start()
|
||||
testNotifTimer2.start()
|
||||
}
|
||||
|
||||
function sendTestNotification(index) {
|
||||
const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]]
|
||||
|
||||
if (index < 0 || index >= notifications.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const notif = notifications[index]
|
||||
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]]
|
||||
testNotificationProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
function setMatugenScheme(scheme) {
|
||||
var normalized = scheme || "scheme-tonal-spot"
|
||||
if (matugenScheme === normalized) return
|
||||
set("matugenScheme", normalized)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setRunUserMatugenTemplates(enabled) {
|
||||
if (runUserMatugenTemplates === enabled) return
|
||||
set("runUserMatugenTemplates", enabled)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setMatugenTargetMonitor(monitorName) {
|
||||
if (matugenTargetMonitor === monitorName) return
|
||||
set("matugenTargetMonitor", monitorName)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setCornerRadius(radius) {
|
||||
set("cornerRadius", radius)
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
|
||||
function setWeatherLocation(displayName, coordinates) {
|
||||
weatherLocation = displayName
|
||||
weatherCoordinates = coordinates
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setIconTheme(themeName) {
|
||||
iconTheme = themeName
|
||||
updateGtkIconTheme()
|
||||
updateQtIconTheme()
|
||||
saveSettings()
|
||||
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
|
||||
function setGtkThemingEnabled(enabled) {
|
||||
set("gtkThemingEnabled", enabled)
|
||||
if (enabled && typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setQtThemingEnabled(enabled) {
|
||||
set("qtThemingEnabled", enabled)
|
||||
if (enabled && typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setShowDock(enabled) {
|
||||
showDock = enabled
|
||||
if (enabled && dockPosition === dankBarPosition) {
|
||||
if (dankBarPosition === SettingsData.Position.Top) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Bottom) {
|
||||
setDockPosition(SettingsData.Position.Top)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Left) {
|
||||
setDockPosition(SettingsData.Position.Right)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Right) {
|
||||
setDockPosition(SettingsData.Position.Left)
|
||||
return
|
||||
}
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDockPosition(position) {
|
||||
dockPosition = position
|
||||
if (position === SettingsData.Position.Bottom && dankBarPosition === SettingsData.Position.Bottom && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Top)
|
||||
}
|
||||
if (position === SettingsData.Position.Top && dankBarPosition === SettingsData.Position.Top && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Bottom)
|
||||
}
|
||||
if (position === SettingsData.Position.Left && dankBarPosition === SettingsData.Position.Left && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Right)
|
||||
}
|
||||
if (position === SettingsData.Position.Right && dankBarPosition === SettingsData.Position.Right && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Left)
|
||||
}
|
||||
saveSettings()
|
||||
Qt.callLater(() => forceDockLayoutRefresh())
|
||||
}
|
||||
|
||||
function setDankBarSpacing(spacing) {
|
||||
set("dankBarSpacing", spacing)
|
||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function setDankBarPosition(position) {
|
||||
dankBarPosition = position
|
||||
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
|
||||
setDockPosition(SettingsData.Position.Top)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) {
|
||||
setDockPosition(SettingsData.Position.Right)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) {
|
||||
setDockPosition(SettingsData.Position.Left)
|
||||
return
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDankBarLeftWidgets(order) {
|
||||
dankBarLeftWidgets = order
|
||||
updateListModel(leftWidgetsModel, order)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDankBarCenterWidgets(order) {
|
||||
dankBarCenterWidgets = order
|
||||
updateListModel(centerWidgetsModel, order)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setDankBarRightWidgets(order) {
|
||||
dankBarRightWidgets = order
|
||||
updateListModel(rightWidgetsModel, order)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function resetDankBarWidgetsToDefault() {
|
||||
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
||||
var defaultCenter = ["music", "clock", "weather"]
|
||||
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"]
|
||||
dankBarLeftWidgets = defaultLeft
|
||||
dankBarCenterWidgets = defaultCenter
|
||||
dankBarRightWidgets = defaultRight
|
||||
updateListModel(leftWidgetsModel, defaultLeft)
|
||||
updateListModel(centerWidgetsModel, defaultCenter)
|
||||
updateListModel(rightWidgetsModel, defaultRight)
|
||||
showLauncherButton = true
|
||||
showWorkspaceSwitcher = true
|
||||
showFocusedWindow = true
|
||||
showWeather = true
|
||||
showMusic = true
|
||||
showClipboard = true
|
||||
showCpuUsage = true
|
||||
showMemUsage = true
|
||||
showCpuTemp = true
|
||||
showGpuTemp = true
|
||||
showSystemTray = true
|
||||
showClock = true
|
||||
showNotificationButton = true
|
||||
showBattery = true
|
||||
showControlCenterButton = true
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setWorkspaceNameIcon(workspaceName, iconData) {
|
||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
||||
iconMap[workspaceName] = iconData
|
||||
workspaceNameIcons = iconMap
|
||||
saveSettings()
|
||||
workspaceIconsUpdated()
|
||||
}
|
||||
|
||||
function removeWorkspaceNameIcon(workspaceName) {
|
||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
||||
delete iconMap[workspaceName]
|
||||
workspaceNameIcons = iconMap
|
||||
saveSettings()
|
||||
workspaceIconsUpdated()
|
||||
}
|
||||
|
||||
function getWorkspaceNameIcon(workspaceName) {
|
||||
return workspaceNameIcons[workspaceName] || null
|
||||
}
|
||||
|
||||
function toggleDankBarVisible() {
|
||||
dankBarVisible = !dankBarVisible
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function getPluginSetting(pluginId, key, defaultValue) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
return defaultValue
|
||||
}
|
||||
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
|
||||
}
|
||||
|
||||
function setPluginSetting(pluginId, key, value) {
|
||||
const updated = JSON.parse(JSON.stringify(pluginSettings))
|
||||
if (!updated[pluginId]) {
|
||||
updated[pluginId] = {}
|
||||
}
|
||||
updated[pluginId][key] = value
|
||||
pluginSettings = updated
|
||||
savePluginSettings()
|
||||
}
|
||||
|
||||
function removePluginSettings(pluginId) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
delete pluginSettings[pluginId]
|
||||
savePluginSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginSettingsForPlugin(pluginId) {
|
||||
const settings = pluginSettings[pluginId]
|
||||
return settings ? JSON.parse(JSON.stringify(settings)) : {}
|
||||
}
|
||||
|
||||
|
||||
ListModel {
|
||||
id: leftWidgetsModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: centerWidgetsModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: rightWidgetsModel
|
||||
}
|
||||
|
||||
property Process testNotificationProcess
|
||||
|
||||
testNotificationProcess: Process {
|
||||
command: []
|
||||
running: false
|
||||
}
|
||||
|
||||
property Timer testNotifTimer1
|
||||
|
||||
testNotifTimer1: Timer {
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: sendTestNotification(1)
|
||||
}
|
||||
|
||||
property Timer testNotifTimer2
|
||||
|
||||
testNotifTimer2: Timer {
|
||||
interval: 800
|
||||
repeat: false
|
||||
onTriggered: sendTestNotification(2)
|
||||
}
|
||||
|
||||
property alias settingsFile: settingsFile
|
||||
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
try {
|
||||
const txt = settingsFile.text()
|
||||
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
|
||||
Store.parse(root, obj)
|
||||
Store.migrate(root, obj)
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to reload settings:", e.message)
|
||||
}
|
||||
hasTriedDefaultSettings = false
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode && !hasTriedDefaultSettings) {
|
||||
hasTriedDefaultSettings = true
|
||||
Processes.checkDefaultSettings()
|
||||
} else if (!isGreeterMode) {
|
||||
applyStoredTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: pluginSettingsFile
|
||||
|
||||
path: isGreeterMode ? "" : pluginSettingsPath
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
parsePluginSettings(pluginSettingsFile.text())
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode) {
|
||||
pluginSettings = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool pluginSettingsFileExists: false
|
||||
|
||||
IpcHandler {
|
||||
function reveal(): string {
|
||||
root.setDankBarVisible(true)
|
||||
return "BAR_SHOW_SUCCESS"
|
||||
}
|
||||
|
||||
function hide(): string {
|
||||
root.setDankBarVisible(false)
|
||||
return "BAR_HIDE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.toggleDankBarVisible()
|
||||
return root.dankBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return root.dankBarVisible ? "visible" : "hidden"
|
||||
}
|
||||
|
||||
target: "bar"
|
||||
}
|
||||
}
|
||||
1130
Common/Theme.qml
1130
Common/Theme.qml
File diff suppressed because it is too large
Load Diff
@@ -1,118 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
.import "./SettingsSpec.js" as SpecModule
|
||||
|
||||
function parse(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
for (var k in SPEC) {
|
||||
var spec = SPEC[k];
|
||||
root[k] = spec.def;
|
||||
}
|
||||
|
||||
if (!jsonObj) return;
|
||||
|
||||
for (var k in jsonObj) {
|
||||
if (!SPEC[k]) continue;
|
||||
var raw = jsonObj[k];
|
||||
var spec = SPEC[k];
|
||||
var coerce = spec.coerce;
|
||||
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
|
||||
}
|
||||
}
|
||||
|
||||
function toJson(root) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
var out = {};
|
||||
for (var k in SPEC) {
|
||||
if (SPEC[k].persist === false) continue;
|
||||
out[k] = root[k];
|
||||
}
|
||||
out.configVersion = root.settingsConfigVersion;
|
||||
return out;
|
||||
}
|
||||
|
||||
function migrate(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
if (!jsonObj) return;
|
||||
|
||||
if (jsonObj.themeIndex !== undefined || jsonObj.themeIsDynamic !== undefined) {
|
||||
var themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"];
|
||||
if (jsonObj.themeIsDynamic) {
|
||||
root.currentThemeName = "dynamic";
|
||||
} else if (jsonObj.themeIndex >= 0 && jsonObj.themeIndex < themeNames.length) {
|
||||
root.currentThemeName = themeNames[jsonObj.themeIndex];
|
||||
}
|
||||
console.info("Auto-migrated theme from index", jsonObj.themeIndex, "to", root.currentThemeName);
|
||||
}
|
||||
|
||||
if ((jsonObj.dankBarWidgetOrder && jsonObj.dankBarWidgetOrder.length > 0) ||
|
||||
(jsonObj.topBarWidgetOrder && jsonObj.topBarWidgetOrder.length > 0)) {
|
||||
if (jsonObj.dankBarLeftWidgets === undefined && jsonObj.dankBarCenterWidgets === undefined && jsonObj.dankBarRightWidgets === undefined) {
|
||||
var widgetOrder = jsonObj.dankBarWidgetOrder || jsonObj.topBarWidgetOrder;
|
||||
root.dankBarLeftWidgets = widgetOrder.filter(function(w) { return ["launcherButton", "workspaceSwitcher", "focusedWindow"].indexOf(w) >= 0; });
|
||||
root.dankBarCenterWidgets = widgetOrder.filter(function(w) { return ["clock", "music", "weather"].indexOf(w) >= 0; });
|
||||
root.dankBarRightWidgets = widgetOrder.filter(function(w) { return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].indexOf(w) >= 0; });
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObj.useOSLogo !== undefined) {
|
||||
root.launcherLogoMode = jsonObj.useOSLogo ? "os" : "apps";
|
||||
root.launcherLogoColorOverride = jsonObj.osLogoColorOverride !== undefined ? jsonObj.osLogoColorOverride : "";
|
||||
root.launcherLogoBrightness = jsonObj.osLogoBrightness !== undefined ? jsonObj.osLogoBrightness : 0.5;
|
||||
root.launcherLogoContrast = jsonObj.osLogoContrast !== undefined ? jsonObj.osLogoContrast : 1;
|
||||
}
|
||||
|
||||
if (jsonObj.mediaCompactMode !== undefined && jsonObj.mediaSize === undefined) {
|
||||
root.mediaSize = jsonObj.mediaCompactMode ? 0 : 1;
|
||||
}
|
||||
|
||||
for (var k in SPEC) {
|
||||
var spec = SPEC[k];
|
||||
if (!spec.migrate) continue;
|
||||
for (var i = 0; i < spec.migrate.length; i++) {
|
||||
var oldKey = spec.migrate[i];
|
||||
if (jsonObj[oldKey] !== undefined && jsonObj[k] === undefined) {
|
||||
var raw = jsonObj[oldKey];
|
||||
var coerce = spec.coerce;
|
||||
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObj.dankBarAtBottom !== undefined || jsonObj.topBarAtBottom !== undefined) {
|
||||
var atBottom = jsonObj.dankBarAtBottom !== undefined ? jsonObj.dankBarAtBottom : jsonObj.topBarAtBottom;
|
||||
root.dankBarPosition = atBottom ? 1 : 0;
|
||||
}
|
||||
|
||||
if (jsonObj.pluginSettings !== undefined) {
|
||||
root.pluginSettings = jsonObj.pluginSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function cleanup(fileText) {
|
||||
var getValidKeys = SpecModule.getValidKeys;
|
||||
if (!fileText || !fileText.trim()) return;
|
||||
|
||||
try {
|
||||
var settings = JSON.parse(fileText);
|
||||
var validKeys = getValidKeys();
|
||||
var needsSave = false;
|
||||
|
||||
for (var key in settings) {
|
||||
if (validKeys.indexOf(key) < 0) {
|
||||
console.log("SettingsData: Removing unused key:", key);
|
||||
delete settings[key];
|
||||
needsSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
return needsSave ? JSON.stringify(settings, null, 2) : null;
|
||||
} catch (e) {
|
||||
console.warn("SettingsData: Failed to cleanup unused keys:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
434
DMSShellIPC.qml
434
DMSShellIPC.qml
@@ -1,434 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var powerMenuModalLoader
|
||||
required property var processListModalLoader
|
||||
required property var controlCenterLoader
|
||||
required property var dankDashPopoutLoader
|
||||
required property var notepadSlideoutVariants
|
||||
required property var hyprKeybindsModalLoader
|
||||
required property var dankBarLoader
|
||||
required property var hyprlandOverviewLoader
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
root.powerMenuModalLoader.active = true
|
||||
if (root.powerMenuModalLoader.item)
|
||||
root.powerMenuModalLoader.item.openCentered()
|
||||
|
||||
return "POWERMENU_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (root.powerMenuModalLoader.item)
|
||||
root.powerMenuModalLoader.item.close()
|
||||
|
||||
return "POWERMENU_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
root.powerMenuModalLoader.active = true
|
||||
if (root.powerMenuModalLoader.item) {
|
||||
if (root.powerMenuModalLoader.item.shouldBeVisible) {
|
||||
root.powerMenuModalLoader.item.close()
|
||||
} else {
|
||||
root.powerMenuModalLoader.item.openCentered()
|
||||
}
|
||||
}
|
||||
|
||||
return "POWERMENU_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "powermenu"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
root.processListModalLoader.active = true
|
||||
if (root.processListModalLoader.item)
|
||||
root.processListModalLoader.item.show()
|
||||
|
||||
return "PROCESSLIST_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.processListModalLoader.item)
|
||||
root.processListModalLoader.item.hide()
|
||||
|
||||
return "PROCESSLIST_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.processListModalLoader.active = true
|
||||
if (root.processListModalLoader.item)
|
||||
root.processListModalLoader.item.toggle()
|
||||
|
||||
return "PROCESSLIST_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "processlist"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
if (root.dankBarLoader.item) {
|
||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
||||
return "CONTROL_CENTER_OPEN_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.controlCenterLoader.item) {
|
||||
root.controlCenterLoader.item.close()
|
||||
return "CONTROL_CENTER_CLOSE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
if (root.dankBarLoader.item) {
|
||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
||||
return "CONTROL_CENTER_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "control-center"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(tab: string): string {
|
||||
root.dankDashPopoutLoader.active = true
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
||||
break
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
||||
root.dankDashPopoutLoader.item.dashVisible = true
|
||||
return "DASH_OPEN_SUCCESS"
|
||||
}
|
||||
return "DASH_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
root.dankDashPopoutLoader.item.dashVisible = false
|
||||
return "DASH_CLOSE_SUCCESS"
|
||||
}
|
||||
return "DASH_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(tab: string): string {
|
||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "wallpaper":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 2
|
||||
break
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0
|
||||
break
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
return "DASH_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "DASH_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "dash"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function getFocusedScreenName() {
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
return Hyprland.focusedWorkspace.monitor.name
|
||||
}
|
||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
return NiriService.currentOutput
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function getActiveNotepadInstance() {
|
||||
if (root.notepadSlideoutVariants.instances.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (root.notepadSlideoutVariants.instances.length === 1) {
|
||||
return root.notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
var focusedScreen = getFocusedScreenName()
|
||||
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
|
||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
||||
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
||||
if (slideout.isVisible) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
|
||||
return root.notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
function open(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.show()
|
||||
return "NOTEPAD_OPEN_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.hide()
|
||||
return "NOTEPAD_CLOSE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.toggle()
|
||||
return "NOTEPAD_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "notepad"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle(): string {
|
||||
SessionService.toggleIdleInhibit()
|
||||
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
|
||||
}
|
||||
|
||||
function enable(): string {
|
||||
SessionService.enableIdleInhibit()
|
||||
return "Idle inhibit enabled"
|
||||
}
|
||||
|
||||
function disable(): string {
|
||||
SessionService.disableIdleInhibit()
|
||||
return "Idle inhibit disabled"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
|
||||
}
|
||||
|
||||
function reason(newReason: string): string {
|
||||
if (!newReason) {
|
||||
return `Current reason: ${SessionService.inhibitReason}`
|
||||
}
|
||||
|
||||
SessionService.setInhibitReason(newReason)
|
||||
return `Inhibit reason set to: ${newReason}`
|
||||
}
|
||||
|
||||
target: "inhibit"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function list(): string {
|
||||
return MprisController.availablePlayers.map(p => p.identity).join("\n")
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
|
||||
MprisController.activePlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
|
||||
MprisController.activePlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
|
||||
MprisController.activePlayer.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
||||
MprisController.activePlayer.previous()
|
||||
}
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
|
||||
MprisController.activePlayer.next()
|
||||
}
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
if (MprisController.activePlayer) {
|
||||
MprisController.activePlayer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
target: "mpris"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle(provider: string): string {
|
||||
if (!provider) {
|
||||
return "ERROR: No provider specified"
|
||||
}
|
||||
|
||||
KeybindsService.loadProvider(provider)
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
} else {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
}
|
||||
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`
|
||||
}
|
||||
return `KEYBINDS_TOGGLE_FAILED: ${provider}`
|
||||
}
|
||||
|
||||
function open(provider: string): string {
|
||||
if (!provider) {
|
||||
return "ERROR: No provider specified"
|
||||
}
|
||||
|
||||
KeybindsService.loadProvider(provider)
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
return `KEYBINDS_OPEN_SUCCESS: ${provider}`
|
||||
}
|
||||
return `KEYBINDS_OPEN_FAILED: ${provider}`
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
return "KEYBINDS_CLOSE_SUCCESS"
|
||||
}
|
||||
return "KEYBINDS_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
target: "keybinds"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
KeybindsService.loadProvider("hyprland")
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
return "HYPR_KEYBINDS_OPEN_SUCCESS"
|
||||
}
|
||||
return "HYPR_KEYBINDS_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function closeBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
|
||||
}
|
||||
return "HYPR_KEYBINDS_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggleBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
KeybindsService.loadProvider("hyprland")
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
} else {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
}
|
||||
return "HYPR_KEYBINDS_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "HYPR_KEYBINDS_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
function toggleOverview(): string {
|
||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
||||
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function closeOverview(): string {
|
||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = false
|
||||
return "OVERVIEW_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function openOverview(): string {
|
||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = true
|
||||
return "OVERVIEW_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
target: "hypr"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function wallpaper(): string {
|
||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||
return "SUCCESS: Toggled wallpaper browser"
|
||||
}
|
||||
return "ERROR: Failed to toggle wallpaper browser"
|
||||
}
|
||||
|
||||
target: "dankdash"
|
||||
}
|
||||
}
|
||||
3
LICENSE
3
LICENSE
@@ -17,4 +17,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string layerNamespace: "dms:modal"
|
||||
WlrLayershell.namespace: layerNamespace
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property Item directContent: null
|
||||
property real width: 400
|
||||
property real height: 300
|
||||
readonly property real screenWidth: screen ? screen.width : 1920
|
||||
readonly property real screenHeight: screen ? screen.height : 1080
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
property bool showBackground: true
|
||||
property real backgroundOpacity: 0.5
|
||||
property string positioning: "center"
|
||||
property point customPosition: Qt.point(0, 0)
|
||||
property bool closeOnEscapeKey: true
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 1
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
property bool enableShadow: false
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
property bool allowFocusOverride: false
|
||||
property bool allowStacking: false
|
||||
property bool keepContentLoaded: false
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
function open() {
|
||||
ModalManager.openModal(root)
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
shouldHaveFocus = false
|
||||
Qt.callLater(() => {
|
||||
shouldHaveFocus = Qt.binding(() => shouldBeVisible)
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false
|
||||
shouldHaveFocus = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
close()
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
visible: shouldBeVisible
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
default:
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
onVisibleChanged: {
|
||||
if (root.visible) {
|
||||
opened()
|
||||
} else {
|
||||
if (Qt.inputMethod) {
|
||||
Qt.inputMethod.hide()
|
||||
Qt.inputMethod.reset()
|
||||
}
|
||||
dialogClosed()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCloseAllModalsExcept(excludedModal) {
|
||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
target: ModalManager
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
|
||||
interval: animationDuration + 120
|
||||
onTriggered: {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: mouse => {
|
||||
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
||||
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
||||
root.backgroundClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.showBackground && SettingsData.modalDarkenBackground
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
|
||||
width: Theme.px(root.width, dpr)
|
||||
height: Theme.px(root.height, dpr)
|
||||
x: {
|
||||
if (positioning === "center") {
|
||||
return Theme.snap((root.screenWidth - width) / 2, dpr)
|
||||
} else if (positioning === "top-right") {
|
||||
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
|
||||
} else if (positioning === "custom") {
|
||||
return Theme.snap(root.customPosition.x, dpr)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
y: {
|
||||
if (positioning === "center") {
|
||||
return Theme.snap((root.screenHeight - height) / 2, dpr)
|
||||
} else if (positioning === "top-right") {
|
||||
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
|
||||
} else if (positioning === "custom") {
|
||||
return Theme.snap(root.customPosition.y, dpr)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr)
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr)
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
layer.enabled: true
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
radius: root.cornerRadius
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
|
||||
objectName: "modalFocusScope"
|
||||
anchors.fill: parent
|
||||
visible: root.shouldBeVisible || root.visible
|
||||
focus: root.shouldBeVisible
|
||||
Keys.onEscapePressed: event => {
|
||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,934 +0,0 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: fileBrowserModal
|
||||
|
||||
layerNamespace: "dms:file-browser"
|
||||
|
||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
|
||||
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
|
||||
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
|
||||
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
|
||||
property string currentPath: ""
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property bool showHiddenFiles: false
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool backButtonFocused: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property int keyboardSelectionIndex: -1
|
||||
property bool keyboardSelectionRequested: false
|
||||
property bool showKeyboardHints: false
|
||||
property bool showFileInfo: false
|
||||
property string selectedFilePath: ""
|
||||
property string selectedFileName: ""
|
||||
property bool selectedFileIsDir: false
|
||||
property bool showOverwriteConfirmation: false
|
||||
property string pendingFilePath: ""
|
||||
property var parentModal: null
|
||||
property bool showSidebar: true
|
||||
property string viewMode: "grid"
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property int iconSizeIndex: 1
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
property int actualGridColumns: 5
|
||||
property bool _initialized: false
|
||||
|
||||
signal fileSelected(string path)
|
||||
|
||||
function loadSettings() {
|
||||
const type = browserType || "default"
|
||||
const settings = CacheData.fileBrowserSettings[type]
|
||||
const isImageBrowser = ["wallpaper", "profile"].includes(browserType)
|
||||
|
||||
if (settings) {
|
||||
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list")
|
||||
sortBy = settings.sortBy || "name"
|
||||
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true
|
||||
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1
|
||||
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true
|
||||
} else {
|
||||
viewMode = isImageBrowser ? "grid" : "list"
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!_initialized)
|
||||
return
|
||||
|
||||
const type = browserType || "default"
|
||||
let settings = CacheData.fileBrowserSettings
|
||||
if (!settings[type]) {
|
||||
settings[type] = {}
|
||||
}
|
||||
settings[type].viewMode = viewMode
|
||||
settings[type].sortBy = sortBy
|
||||
settings[type].sortAscending = sortAscending
|
||||
settings[type].iconSizeIndex = iconSizeIndex
|
||||
settings[type].showSidebar = showSidebar
|
||||
settings[type].lastPath = currentPath
|
||||
CacheData.fileBrowserSettings = settings
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = currentPath
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = currentPath
|
||||
}
|
||||
|
||||
CacheData.saveCache()
|
||||
}
|
||||
|
||||
onViewModeChanged: saveSettings()
|
||||
onSortByChanged: saveSettings()
|
||||
onSortAscendingChanged: saveSettings()
|
||||
onIconSizeIndexChanged: saveSettings()
|
||||
onShowSidebarChanged: saveSettings()
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false
|
||||
}
|
||||
const ext = fileName.toLowerCase().split('.').pop()
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
const type = browserType || "default"
|
||||
const settings = CacheData.fileBrowserSettings[type]
|
||||
const lastPath = settings?.lastPath || ""
|
||||
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
||||
}
|
||||
|
||||
function saveLastPath(path) {
|
||||
const type = browserType || "default"
|
||||
let settings = CacheData.fileBrowserSettings
|
||||
if (!settings[type]) {
|
||||
settings[type] = {}
|
||||
}
|
||||
settings[type].lastPath = path
|
||||
CacheData.fileBrowserSettings = settings
|
||||
CacheData.saveCache()
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = path
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = path
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedFileData(path, name, isDir) {
|
||||
selectedFilePath = path
|
||||
selectedFileName = name
|
||||
selectedFileIsDir = isDir
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
const path = currentPath
|
||||
if (path === homeDir)
|
||||
return
|
||||
|
||||
const lastSlash = path.lastIndexOf('/')
|
||||
if (lastSlash > 0) {
|
||||
const newPath = path.substring(0, lastSlash)
|
||||
if (newPath.length < homeDir.length) {
|
||||
currentPath = homeDir
|
||||
saveLastPath(homeDir)
|
||||
} else {
|
||||
currentPath = newPath
|
||||
saveLastPath(newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
currentPath = path
|
||||
saveLastPath(path)
|
||||
selectedIndex = -1
|
||||
backButtonFocused = false
|
||||
}
|
||||
|
||||
function keyboardFileSelection(index) {
|
||||
if (index >= 0) {
|
||||
keyboardSelectionTimer.targetIndex = index
|
||||
keyboardSelectionTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
function executeKeyboardSelection(index) {
|
||||
keyboardSelectionIndex = index
|
||||
keyboardSelectionRequested = true
|
||||
}
|
||||
|
||||
function handleSaveFile(filePath) {
|
||||
var normalizedPath = filePath
|
||||
if (!normalizedPath.startsWith("file://")) {
|
||||
normalizedPath = "file://" + filePath
|
||||
}
|
||||
|
||||
var exists = false
|
||||
var fileName = filePath.split('/').pop()
|
||||
|
||||
for (var i = 0; i < folderModel.count; i++) {
|
||||
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
pendingFilePath = normalizedPath
|
||||
showOverwriteConfirmation = true
|
||||
} else {
|
||||
fileSelected(normalizedPath)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
|
||||
objectName: "fileBrowserModal"
|
||||
allowStacking: true
|
||||
closeOnEscapeKey: false
|
||||
shouldHaveFocus: shouldBeVisible
|
||||
Component.onCompleted: {
|
||||
loadSettings()
|
||||
currentPath = getLastPath()
|
||||
_initialized = true
|
||||
}
|
||||
|
||||
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
|
||||
property int currentPathIndex: 0
|
||||
|
||||
width: 800
|
||||
height: 600
|
||||
enableShadow: true
|
||||
visible: false
|
||||
onBackgroundClicked: close()
|
||||
onOpened: {
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = false
|
||||
parentModal.allowFocusOverride = true
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader && contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
onDialogClosed: {
|
||||
if (parentModal) {
|
||||
parentModal.allowFocusOverride = false
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
currentPath = getLastPath()
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
backButtonFocused = false
|
||||
}
|
||||
}
|
||||
onCurrentPathChanged: {
|
||||
selectedFilePath = ""
|
||||
selectedFileName = ""
|
||||
selectedFileIsDir = false
|
||||
saveSettings()
|
||||
}
|
||||
onSelectedIndexChanged: {
|
||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||
selectedFilePath = ""
|
||||
selectedFileName = ""
|
||||
selectedFileIsDir = false
|
||||
}
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
|
||||
showDirsFirst: true
|
||||
showDotAndDotDot: false
|
||||
showHidden: fileBrowserModal.showHiddenFiles
|
||||
nameFilters: fileExtensions
|
||||
showFiles: true
|
||||
showDirs: true
|
||||
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
||||
sortField: {
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return FolderListModel.Name
|
||||
case "size":
|
||||
return FolderListModel.Size
|
||||
case "modified":
|
||||
return FolderListModel.Time
|
||||
case "type":
|
||||
return FolderListModel.Type
|
||||
default:
|
||||
return FolderListModel.Name
|
||||
}
|
||||
}
|
||||
sortReversed: !sortAscending
|
||||
}
|
||||
|
||||
property var quickAccessLocations: [{
|
||||
"name": "Home",
|
||||
"path": homeDir,
|
||||
"icon": "home"
|
||||
}, {
|
||||
"name": "Documents",
|
||||
"path": docsDir,
|
||||
"icon": "description"
|
||||
}, {
|
||||
"name": "Downloads",
|
||||
"path": downloadDir,
|
||||
"icon": "download"
|
||||
}, {
|
||||
"name": "Pictures",
|
||||
"path": picsDir,
|
||||
"icon": "image"
|
||||
}, {
|
||||
"name": "Music",
|
||||
"path": musicDir,
|
||||
"icon": "music_note"
|
||||
}, {
|
||||
"name": "Videos",
|
||||
"path": videosDir,
|
||||
"icon": "movie"
|
||||
}, {
|
||||
"name": "Desktop",
|
||||
"path": desktopDir,
|
||||
"icon": "computer"
|
||||
}]
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
property int totalItems: folderModel.count
|
||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close()
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
showKeyboardHints = !showKeyboardHints
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||
showFileInfo = !showFileInfo
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||
if (currentPath !== homeDir) {
|
||||
navigateUp()
|
||||
event.accepted = true
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!keyboardNavigationActive) {
|
||||
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key
|
||||
=== Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
|
||||
|
||||
if (isInitKey) {
|
||||
keyboardNavigationActive = true
|
||||
if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
} else {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
return
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
} else {
|
||||
selectedIndex = 0
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Backtab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = totalItems - 1
|
||||
} else if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
} else {
|
||||
selectedIndex = totalItems - 1
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_H:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (!backButtonFocused && selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_L:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_Left:
|
||||
if (pathInputHasFocus)
|
||||
return
|
||||
if (backButtonFocused)
|
||||
return
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Right:
|
||||
if (pathInputHasFocus)
|
||||
return
|
||||
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Up:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
if (gridColumns === 1) {
|
||||
selectedIndex = 0
|
||||
} else {
|
||||
var col = selectedIndex % gridColumns
|
||||
selectedIndex = Math.min(col, totalItems - 1)
|
||||
}
|
||||
} else if (selectedIndex >= gridColumns) {
|
||||
selectedIndex -= gridColumns
|
||||
} else if (selectedIndex > 0 && gridColumns === 1) {
|
||||
selectedIndex--
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true
|
||||
selectedIndex = -1
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false
|
||||
selectedIndex = 0
|
||||
} else if (gridColumns === 1) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
} else {
|
||||
var newIndex = selectedIndex + gridColumns
|
||||
if (newIndex < totalItems) {
|
||||
selectedIndex = newIndex
|
||||
} else {
|
||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
||||
var col = selectedIndex % gridColumns
|
||||
var targetIndex = lastRowStart + col
|
||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||
selectedIndex = targetIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Space:
|
||||
if (backButtonFocused)
|
||||
navigateUp()
|
||||
else if (selectedIndex >= 0 && selectedIndex < totalItems)
|
||||
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
||||
event.accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: keyboardSelectionTimer
|
||||
|
||||
property int targetIndex: -1
|
||||
|
||||
interval: 1
|
||||
onTriggered: {
|
||||
executeKeyboardSelection(targetIndex)
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onPressed: event => {
|
||||
keyboardController.handleKey(event)
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: browserIcon
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: browserTitle
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: showHiddenFiles ? "visibility_off" : "visibility"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
|
||||
onClicked: showHiddenFiles = !showHiddenFiles
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: viewMode === "grid" ? "view_list" : "grid_view"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: viewMode === "grid"
|
||||
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 49
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Row {
|
||||
width: showSidebar ? 201 : 0
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
visible: showSidebar
|
||||
|
||||
FileBrowserSidebar {
|
||||
height: parent.height
|
||||
quickAccessLocations: fileBrowserModal.quickAccessLocations
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
onLocationSelected: path => navigateTo(path)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 1
|
||||
height: parent.height
|
||||
color: Theme.outline
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - (showSidebar ? 201 : 0)
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
FileBrowserNavigation {
|
||||
width: parent.width
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
homeDir: fileBrowserModal.homeDir
|
||||
backButtonFocused: fileBrowserModal.backButtonFocused
|
||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
||||
showSidebar: fileBrowserModal.showSidebar
|
||||
pathEditMode: fileBrowserModal.pathEditMode
|
||||
onNavigateUp: fileBrowserModal.navigateUp()
|
||||
onNavigateTo: path => fileBrowserModal.navigateTo(path)
|
||||
onPathInputFocusChanged: hasFocus => {
|
||||
fileBrowserModal.pathInputHasFocus = hasFocus
|
||||
if (hasFocus) {
|
||||
fileBrowserModal.pathEditMode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridContainer
|
||||
width: parent.width
|
||||
height: parent.height - 41
|
||||
clip: true
|
||||
|
||||
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
|
||||
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
|
||||
property real availableGridWidth: width - Theme.spacingM * 2
|
||||
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
|
||||
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
|
||||
|
||||
onGridColumnsChanged: {
|
||||
fileBrowserModal.actualGridColumns = gridColumns
|
||||
}
|
||||
Component.onCompleted: {
|
||||
fileBrowserModal.actualGridColumns = gridColumns
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: fileGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: gridContainer.gridLeftMargin
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "grid"
|
||||
cellWidth: gridContainer.gridCellWidth
|
||||
cellHeight: gridContainer.gridCellHeight
|
||||
cacheBuffer: 260
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, GridView.Contain)
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: gridScrollbar
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: FileBrowserGridDelegate {
|
||||
iconSizes: fileBrowserModal.iconSizes
|
||||
iconSizeIndex: fileBrowserModal.iconSizeIndex
|
||||
selectedIndex: fileBrowserModal.selectedIndex
|
||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index
|
||||
setSelectedFileData(path, name, isDir)
|
||||
if (isDir) {
|
||||
navigateTo(path)
|
||||
} else {
|
||||
fileSelected(path)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir)
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
|
||||
fileBrowserModal.keyboardSelectionRequested = false
|
||||
selectedIndex = index
|
||||
setSelectedFileData(filePath, fileName, fileIsDir)
|
||||
if (fileIsDir) {
|
||||
navigateTo(filePath)
|
||||
} else {
|
||||
fileSelected(filePath)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target: fileBrowserModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: fileList
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "list"
|
||||
spacing: 2
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: listScrollbar
|
||||
}
|
||||
|
||||
delegate: FileBrowserListDelegate {
|
||||
width: fileList.width
|
||||
selectedIndex: fileBrowserModal.selectedIndex
|
||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index
|
||||
setSelectedFileData(path, name, isDir)
|
||||
if (isDir) {
|
||||
navigateTo(path)
|
||||
} else {
|
||||
fileSelected(path)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir)
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
|
||||
fileBrowserModal.keyboardSelectionRequested = false
|
||||
selectedIndex = index
|
||||
setSelectedFileData(filePath, fileName, fileIsDir)
|
||||
if (fileIsDir) {
|
||||
navigateTo(filePath)
|
||||
} else {
|
||||
fileSelected(filePath)
|
||||
fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target: fileBrowserModal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSaveRow {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
saveMode: fileBrowserModal.saveMode
|
||||
defaultFileName: fileBrowserModal.defaultFileName
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
onSaveRequested: filePath => handleSaveFile(filePath)
|
||||
}
|
||||
|
||||
KeyboardHints {
|
||||
id: keyboardHints
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: fileBrowserModal.showKeyboardHints
|
||||
}
|
||||
|
||||
FileInfo {
|
||||
id: fileInfo
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
width: 300
|
||||
showFileInfo: fileBrowserModal.showFileInfo
|
||||
selectedIndex: fileBrowserModal.selectedIndex
|
||||
sourceFolderModel: folderModel
|
||||
currentPath: fileBrowserModal.currentPath
|
||||
currentFileName: fileBrowserModal.selectedFileName
|
||||
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
||||
currentFileExtension: {
|
||||
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
|
||||
return ""
|
||||
|
||||
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
||||
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSortMenu {
|
||||
id: sortMenu
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 120
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
sortBy: fileBrowserModal.sortBy
|
||||
sortAscending: fileBrowserModal.sortAscending
|
||||
onSortBySelected: value => {
|
||||
fileBrowserModal.sortBy = value
|
||||
}
|
||||
onSortOrderSelected: ascending => {
|
||||
fileBrowserModal.sortAscending = ascending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserOverwriteDialog {
|
||||
anchors.fill: parent
|
||||
showDialog: showOverwriteConfirmation
|
||||
pendingFilePath: fileBrowserModal.pendingFilePath
|
||||
onConfirmed: filePath => {
|
||||
showOverwriteConfirmation = false
|
||||
fileSelected(filePath)
|
||||
pendingFilePath = ""
|
||||
Qt.callLater(() => fileBrowserModal.close())
|
||||
}
|
||||
onCancelled: {
|
||||
showOverwriteConfirmation = false
|
||||
pendingFilePath = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:keybinds"
|
||||
property real scrollStep: 60
|
||||
property var activeFlickable: null
|
||||
property real _maxW: Math.min(Screen.width * 0.92, 1200)
|
||||
property real _maxH: Math.min(Screen.height * 0.92, 900)
|
||||
width: _maxW
|
||||
height: _maxH
|
||||
onBackgroundClicked: close()
|
||||
|
||||
function scrollDown() {
|
||||
if (!root.activeFlickable) return
|
||||
let newY = root.activeFlickable.contentY + scrollStep
|
||||
newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height)
|
||||
root.activeFlickable.contentY = newY
|
||||
}
|
||||
|
||||
function scrollUp() {
|
||||
if (!root.activeFlickable) return
|
||||
let newY = root.activeFlickable.contentY - root.scrollStep
|
||||
newY = Math.max(0, newY)
|
||||
root.activeFlickable.contentY = newY
|
||||
}
|
||||
|
||||
Shortcut { sequence: "Ctrl+j"; onActivated: root.scrollDown() }
|
||||
Shortcut { sequence: "Down"; onActivated: root.scrollDown() }
|
||||
Shortcut { sequence: "Ctrl+k"; onActivated: root.scrollUp() }
|
||||
Shortcut { sequence: "Up"; onActivated: root.scrollUp() }
|
||||
Shortcut { sequence: "Esc"; onActivated: root.close() }
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
StyledText {
|
||||
text: KeybindsService.keybinds.title || "Keybinds"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
width: parent.width
|
||||
height: parent.height - parent.spacing - 40
|
||||
contentWidth: rowLayout.implicitWidth
|
||||
contentHeight: rowLayout.implicitHeight
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: root.activeFlickable = mainFlickable
|
||||
|
||||
property var rawBinds: KeybindsService.keybinds.binds || {}
|
||||
property var categories: {
|
||||
const processed = {}
|
||||
for (const cat in rawBinds) {
|
||||
const binds = rawBinds[cat]
|
||||
const subcats = {}
|
||||
let hasSubcats = false
|
||||
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i]
|
||||
if (bind.subcat) {
|
||||
hasSubcats = true
|
||||
if (!subcats[bind.subcat]) {
|
||||
subcats[bind.subcat] = []
|
||||
}
|
||||
subcats[bind.subcat].push(bind)
|
||||
} else {
|
||||
if (!subcats["_root"]) {
|
||||
subcats["_root"] = []
|
||||
}
|
||||
subcats["_root"].push(bind)
|
||||
}
|
||||
}
|
||||
|
||||
processed[cat] = {
|
||||
hasSubcats: hasSubcats,
|
||||
subcats: subcats,
|
||||
subcatKeys: Object.keys(subcats)
|
||||
}
|
||||
}
|
||||
return processed
|
||||
}
|
||||
property var categoryKeys: Object.keys(categories)
|
||||
|
||||
function distributeCategories(cols) {
|
||||
const columns = []
|
||||
for (let i = 0; i < cols; i++) {
|
||||
columns.push([])
|
||||
}
|
||||
for (let i = 0; i < categoryKeys.length; i++) {
|
||||
columns[i % cols].push(categoryKeys[i])
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
width: mainFlickable.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
|
||||
property var columnCategories: mainFlickable.distributeCategories(numColumns)
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.numColumns
|
||||
|
||||
Column {
|
||||
id: masonryColumn
|
||||
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.columnCategories[index] || []
|
||||
|
||||
Column {
|
||||
id: categoryColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string catName: modelData
|
||||
property var catData: mainFlickable.categories[catName]
|
||||
|
||||
StyledText {
|
||||
text: categoryColumn.catName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.spacingXS }
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: categoryColumn.catData?.subcatKeys || []
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string subcatName: modelData
|
||||
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
|
||||
|
||||
StyledText {
|
||||
visible: parent.subcatName !== "_root"
|
||||
text: parent.subcatName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.primary
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: parent.parent.subcatBinds
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.9
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
color: Theme.secondary
|
||||
text: modelData.key || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: modelData.desc || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:polkit"
|
||||
|
||||
property string passwordInput: ""
|
||||
property var currentFlow: PolkitService.agent?.flow
|
||||
property bool isLoading: false
|
||||
property real minHeight: 240
|
||||
|
||||
function show() {
|
||||
passwordInput = ""
|
||||
isLoading = false
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.passwordField) {
|
||||
contentLoader.item.passwordField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240)
|
||||
|
||||
Connections {
|
||||
target: contentLoader.item
|
||||
function onImplicitHeightChanged() {
|
||||
if (shouldBeVisible && contentLoader.item) {
|
||||
const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2
|
||||
if (newHeight > minHeight) {
|
||||
minHeight = newHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.passwordField) {
|
||||
contentLoader.item.passwordField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
passwordInput = ""
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
onBackgroundClicked: () => {
|
||||
if (currentFlow && !isLoading) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PolkitService.agent
|
||||
enabled: PolkitService.polkitAvailable
|
||||
|
||||
function onAuthenticationRequestStarted() {
|
||||
show()
|
||||
}
|
||||
|
||||
function onIsActiveChanged() {
|
||||
if (!(PolkitService.agent?.isActive ?? false)) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentFlow
|
||||
enabled: currentFlow !== null
|
||||
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (currentFlow.isResponseRequired) {
|
||||
isLoading = false
|
||||
passwordInput = ""
|
||||
if (contentLoader.item && contentLoader.item.passwordField) {
|
||||
contentLoader.item.passwordField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthenticationSucceeded() {
|
||||
close()
|
||||
}
|
||||
|
||||
function onAuthenticationFailed() {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
function onAuthenticationRequestCancelled() {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: authContent
|
||||
|
||||
property alias passwordField: passwordField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
implicitHeight: headerRow.implicitHeight + mainColumn.implicitHeight + Theme.spacingM
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
if (currentFlow && !isLoading) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication Required")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.message ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: (currentFlow?.supplementaryMessage ?? "") !== ""
|
||||
text: currentFlow?.supplementaryMessage ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: () => {
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: currentFlow?.inputPrompt ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
visible: (currentFlow?.inputPrompt ?? "") !== ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: passwordField.activeFocus ? 2 : 1
|
||||
opacity: isLoading ? 0.5 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !isLoading
|
||||
onClicked: () => {
|
||||
passwordField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: passwordInput
|
||||
echoMode: (currentFlow?.responseVisible ?? false) ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
backgroundColor: "transparent"
|
||||
enabled: !isLoading
|
||||
onTextEdited: () => {
|
||||
passwordInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
if (passwordInput.length > 0 && currentFlow && !isLoading) {
|
||||
isLoading = true
|
||||
currentFlow.submit(passwordInput)
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: (currentFlow?.failed ?? false) ? failedText.implicitHeight : 0
|
||||
visible: height > 0
|
||||
|
||||
StyledText {
|
||||
id: failedText
|
||||
text: I18n.tr("Authentication failed, please try again")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
opacity: (currentFlow?.failed ?? false) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
enabled: !isLoading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, authText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: !isLoading && (passwordInput.length > 0 || !(currentFlow?.isResponseRequired ?? true))
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: authText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Authenticate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: authArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
if (currentFlow && !isLoading) {
|
||||
isLoading = true
|
||||
currentFlow.submit(passwordInput)
|
||||
passwordInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:power-menu"
|
||||
|
||||
property int selectedRow: 0
|
||||
property int selectedCol: 0
|
||||
property int selectedIndex: selectedRow * gridColumns + selectedCol
|
||||
property rect parentBounds: Qt.rect(0, 0, 0, 0)
|
||||
property var parentScreen: null
|
||||
property var visibleActions: []
|
||||
property int gridColumns: 3
|
||||
property int gridRows: 2
|
||||
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
signal lockRequested
|
||||
|
||||
function openCentered() {
|
||||
parentBounds = Qt.rect(0, 0, 0, 0)
|
||||
parentScreen = null
|
||||
backgroundOpacity = 0.5
|
||||
open()
|
||||
}
|
||||
|
||||
function openFromControlCenter(bounds, targetScreen) {
|
||||
parentBounds = bounds
|
||||
parentScreen = targetScreen
|
||||
backgroundOpacity = 0
|
||||
open()
|
||||
}
|
||||
|
||||
function updateVisibleActions() {
|
||||
const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
visibleActions = allActions.filter(action => {
|
||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||
return false
|
||||
return true
|
||||
})
|
||||
|
||||
const count = visibleActions.length
|
||||
switch (count) {
|
||||
case 0:
|
||||
gridColumns = 1
|
||||
gridRows = 1
|
||||
break
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
gridColumns = 1
|
||||
gridRows = count
|
||||
break
|
||||
case 4:
|
||||
gridColumns = 2
|
||||
gridRows = 2
|
||||
break
|
||||
default:
|
||||
gridColumns = 3
|
||||
gridRows = Math.ceil(count / 3)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultActionIndex() {
|
||||
const defaultAction = SettingsData.powerMenuDefaultAction || "logout"
|
||||
const index = visibleActions.indexOf(defaultAction)
|
||||
return index >= 0 ? index : 0
|
||||
}
|
||||
|
||||
function getActionAtIndex(index) {
|
||||
if (index < 0 || index >= visibleActions.length)
|
||||
return ""
|
||||
return visibleActions[index]
|
||||
}
|
||||
|
||||
function getActionData(action) {
|
||||
switch (action) {
|
||||
case "reboot":
|
||||
return {
|
||||
"icon": "restart_alt",
|
||||
"label": I18n.tr("Reboot"),
|
||||
"key": "R"
|
||||
}
|
||||
case "logout":
|
||||
return {
|
||||
"icon": "logout",
|
||||
"label": I18n.tr("Log Out"),
|
||||
"key": "X"
|
||||
}
|
||||
case "poweroff":
|
||||
return {
|
||||
"icon": "power_settings_new",
|
||||
"label": I18n.tr("Power Off"),
|
||||
"key": "P"
|
||||
}
|
||||
case "lock":
|
||||
return {
|
||||
"icon": "lock",
|
||||
"label": I18n.tr("Lock"),
|
||||
"key": "L"
|
||||
}
|
||||
case "suspend":
|
||||
return {
|
||||
"icon": "bedtime",
|
||||
"label": I18n.tr("Suspend"),
|
||||
"key": "S"
|
||||
}
|
||||
case "hibernate":
|
||||
return {
|
||||
"icon": "ac_unit",
|
||||
"label": I18n.tr("Hibernate"),
|
||||
"key": "H"
|
||||
}
|
||||
case "restart":
|
||||
return {
|
||||
"icon": "refresh",
|
||||
"label": I18n.tr("Restart DMS"),
|
||||
"key": "D"
|
||||
}
|
||||
default:
|
||||
return {
|
||||
"icon": "help",
|
||||
"label": action,
|
||||
"key": "?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectOption(action) {
|
||||
if (action === "lock") {
|
||||
close()
|
||||
lockRequested()
|
||||
return
|
||||
}
|
||||
if (action === "restart") {
|
||||
close()
|
||||
Quickshell.execDetached(["dms", "restart"])
|
||||
return
|
||||
}
|
||||
close()
|
||||
const actions = {
|
||||
"logout": {
|
||||
"title": I18n.tr("Log Out"),
|
||||
"message": I18n.tr("Are you sure you want to log out?")
|
||||
},
|
||||
"suspend": {
|
||||
"title": I18n.tr("Suspend"),
|
||||
"message": I18n.tr("Are you sure you want to suspend the system?")
|
||||
},
|
||||
"hibernate": {
|
||||
"title": I18n.tr("Hibernate"),
|
||||
"message": I18n.tr("Are you sure you want to hibernate the system?")
|
||||
},
|
||||
"reboot": {
|
||||
"title": I18n.tr("Reboot"),
|
||||
"message": I18n.tr("Are you sure you want to reboot the system?")
|
||||
},
|
||||
"poweroff": {
|
||||
"title": I18n.tr("Power Off"),
|
||||
"message": I18n.tr("Are you sure you want to power off the system?")
|
||||
}
|
||||
}
|
||||
const selected = actions[action]
|
||||
if (selected) {
|
||||
root.powerActionRequested(action, selected.title, selected.message)
|
||||
}
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2)
|
||||
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
||||
enableShadow: true
|
||||
screen: parentScreen
|
||||
positioning: parentBounds.width > 0 ? "custom" : "center"
|
||||
customPosition: {
|
||||
if (parentBounds.width > 0) {
|
||||
const centerX = parentBounds.x + (parentBounds.width - width) / 2
|
||||
const centerY = parentBounds.y + (parentBounds.height - height) / 2
|
||||
return Qt.point(centerX, centerY)
|
||||
}
|
||||
return Qt.point(0, 0)
|
||||
}
|
||||
onBackgroundClicked: () => close()
|
||||
onOpened: () => {
|
||||
updateVisibleActions()
|
||||
const defaultIndex = getDefaultActionIndex()
|
||||
selectedRow = Math.floor(defaultIndex / gridColumns)
|
||||
selectedCol = defaultIndex % gridColumns
|
||||
Qt.callLater(() => modalFocusScope.forceActiveFocus())
|
||||
}
|
||||
Component.onCompleted: updateVisibleActions()
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Left:
|
||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Right:
|
||||
selectedCol = (selectedCol + 1) % gridColumns
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
selectedRow = (selectedRow + 1) % gridRows
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
selectOption(getActionAtIndex(selectedIndex))
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedCol = (selectedCol + 1) % gridColumns
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (!(event.modifiers & Qt.ControlModifier)) {
|
||||
selectOption("poweroff")
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedRow = (selectedRow + 1) % gridRows
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_R:
|
||||
selectOption("reboot")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_X:
|
||||
selectOption("logout")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_L:
|
||||
selectOption("lock")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_S:
|
||||
selectOption("suspend")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_H:
|
||||
selectOption("hibernate")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_D:
|
||||
selectOption("restart")
|
||||
event.accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
implicitHeight: buttonGrid.implicitHeight + Theme.spacingL * 2
|
||||
|
||||
Grid {
|
||||
id: buttonGrid
|
||||
anchors.centerIn: parent
|
||||
columns: root.gridColumns
|
||||
columnSpacing: Theme.spacingS
|
||||
rowSpacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.visibleActions
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
readonly property bool isSelected: root.selectedIndex === index
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
||||
|
||||
width: (root.width - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
if (mouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
border.color: isSelected ? Theme.primary : "transparent"
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: parent.parent.actionData.icon
|
||||
size: Theme.iconSize + 8
|
||||
color: {
|
||||
if (parent.parent.showWarning && mouseArea.containsMouse) {
|
||||
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.actionData.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (parent.parent.showWarning && mouseArea.containsMouse) {
|
||||
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 16
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.parent.actionData.key
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedRow = Math.floor(index / root.gridColumns)
|
||||
root.selectedCol = index % root.gridColumns
|
||||
root.selectOption(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: processListModal
|
||||
|
||||
layerNamespace: "dms:process-list-modal"
|
||||
|
||||
property int currentTab: 0
|
||||
property var tabNames: ["Processes", "Performance", "System"]
|
||||
|
||||
function show() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
return ;
|
||||
}
|
||||
open();
|
||||
UserInfoService.getUptime();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
if (processContextMenu.visible) {
|
||||
processContextMenu.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
return ;
|
||||
}
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
width: 900
|
||||
height: 680
|
||||
visible: false
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
enableShadow: true
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: processesTabComponent
|
||||
|
||||
ProcessesTab {
|
||||
contextMenu: processContextMenu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
|
||||
PerformanceTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
|
||||
SystemTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ProcessContextMenu {
|
||||
id: processContextMenu
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
processListModal.hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_1) {
|
||||
currentTab = 0;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_2) {
|
||||
currentTab = 1;
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_3) {
|
||||
currentTab = 2;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message when dgop is not available
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 400
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
|
||||
border.color: Theme.error
|
||||
border.width: 2
|
||||
visible: !DgopService.dgopAvailable
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: "error"
|
||||
size: 48
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Monitor Unavailable")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
visible: DgopService.dgopAvailable
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Monitor")
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
return processListModal.hide();
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 52
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: tabNames
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||
border.width: currentTab === index ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
||||
return tabIcons[index] || "tab";
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
opacity: currentTab === index ? 1 : 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -1
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Loader {
|
||||
id: processesTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 0
|
||||
visible: currentTab === 0
|
||||
opacity: currentTab === 0 ? 1 : 0
|
||||
sourceComponent: processesTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: performanceTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 1
|
||||
visible: currentTab === 1
|
||||
opacity: currentTab === 1 ? 1 : 0
|
||||
sourceComponent: performanceTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemTab
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
active: processListModal.visible && currentTab === 2
|
||||
visible: currentTab === 2
|
||||
opacity: currentTab === 2 ? 1 : 0
|
||||
sourceComponent: systemTabComponent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: settingsModal
|
||||
|
||||
layerNamespace: "dms:settings"
|
||||
|
||||
property Component settingsContent
|
||||
property alias profileBrowser: profileBrowser
|
||||
property int currentTabIndex: 0
|
||||
|
||||
signal closingModal()
|
||||
|
||||
function show() {
|
||||
open();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
objectName: "settingsModal"
|
||||
width: Math.min(800, screenWidth * 0.9)
|
||||
height: Math.min(800, screenHeight * 0.85)
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
visible: false
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
}
|
||||
content: settingsContent
|
||||
onOpened: () => {
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
const tabCount = 11
|
||||
if (event.key === Qt.Key_Down) {
|
||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Tab && !event.modifiers) {
|
||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
|
||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
settingsModal.show();
|
||||
return "SETTINGS_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
settingsModal.hide();
|
||||
return "SETTINGS_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
settingsModal.toggle();
|
||||
return "SETTINGS_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "settings"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function browse(type: string) {
|
||||
if (type === "wallpaper") {
|
||||
wallpaperBrowser.allowStacking = false;
|
||||
wallpaperBrowser.open();
|
||||
} else if (type === "profile") {
|
||||
profileBrowser.allowStacking = false;
|
||||
profileBrowser.open();
|
||||
}
|
||||
}
|
||||
|
||||
target: "file"
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: profileBrowser
|
||||
|
||||
allowStacking: true
|
||||
parentModal: settingsModal
|
||||
browserTitle: "Select Profile Image"
|
||||
browserIcon: "person"
|
||||
browserType: "profile"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||
onFileSelected: (path) => {
|
||||
PortalService.setProfileImage(path);
|
||||
close();
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
allowStacking = true;
|
||||
if (settingsModal.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
settingsModal.modalFocusScope.forceActiveFocus()
|
||||
if (settingsModal.contentLoader.item) {
|
||||
settingsModal.contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: wallpaperBrowser
|
||||
|
||||
allowStacking: true
|
||||
parentModal: settingsModal
|
||||
browserTitle: "Select Wallpaper"
|
||||
browserIcon: "wallpaper"
|
||||
browserType: "wallpaper"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
||||
onFileSelected: (path) => {
|
||||
SessionData.setWallpaper(path);
|
||||
close();
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
allowStacking = true;
|
||||
if (settingsModal.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
settingsModal.modalFocusScope.forceActiveFocus()
|
||||
if (settingsModal.contentLoader.item) {
|
||||
settingsModal.contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settingsContent: Component {
|
||||
Item {
|
||||
id: rootScope
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
settingsModal.hide()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 35
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Settings")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
return settingsModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: parent.height - 35
|
||||
spacing: 0
|
||||
|
||||
SettingsSidebar {
|
||||
id: sidebar
|
||||
|
||||
parentModal: settingsModal
|
||||
currentIndex: settingsModal.currentTabIndex
|
||||
onCurrentIndexChanged: {
|
||||
settingsModal.currentTabIndex = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
SettingsContent {
|
||||
id: content
|
||||
|
||||
width: parent.width - sidebar.width
|
||||
height: parent.height
|
||||
parentModal: settingsModal
|
||||
currentIndex: settingsModal.currentTabIndex
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: spotlightKeyHandler
|
||||
|
||||
property alias appLauncher: appLauncher
|
||||
property alias searchField: searchField
|
||||
property alias fileSearchController: fileSearchController
|
||||
property var parentModal: null
|
||||
property string searchMode: "apps"
|
||||
|
||||
function resetScroll() {
|
||||
if (searchMode === "apps") {
|
||||
resultsView.resetScroll()
|
||||
} else {
|
||||
fileSearchResults.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearchMode() {
|
||||
if (searchField.text.startsWith("/")) {
|
||||
if (searchMode !== "files") {
|
||||
searchMode = "files"
|
||||
}
|
||||
const query = searchField.text.substring(1)
|
||||
fileSearchController.searchQuery = query
|
||||
} else {
|
||||
if (searchMode !== "apps") {
|
||||
searchMode = "apps"
|
||||
fileSearchController.reset()
|
||||
appLauncher.searchQuery = searchField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearchModeChanged: {
|
||||
if (searchMode === "files") {
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
} else {
|
||||
fileSearchController.keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
clip: false
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectNext()
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectPrevious()
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectNext()
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.selectPrevious()
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Tab) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
} else {
|
||||
appLauncher.selectNext()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Backtab) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
} else {
|
||||
appLauncher.selectPrevious()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectNextInRow()
|
||||
} else {
|
||||
appLauncher.selectNext()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.viewMode === "grid") {
|
||||
appLauncher.selectPreviousInRow()
|
||||
} else {
|
||||
appLauncher.selectPrevious()
|
||||
}
|
||||
} else {
|
||||
fileSearchController.selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.launchSelected()
|
||||
} else if (searchMode === "files") {
|
||||
fileSearchController.openSelected()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncher
|
||||
|
||||
viewMode: SettingsData.spotlightModalViewMode
|
||||
gridColumns: 4
|
||||
onAppLaunched: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
}
|
||||
onViewModeSelected: mode => {
|
||||
SettingsData.set("spotlightModalViewMode", mode)
|
||||
}
|
||||
}
|
||||
|
||||
FileSearchController {
|
||||
id: fileSearchController
|
||||
|
||||
onFileOpened: () => {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
clip: false
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
leftPadding: Theme.spacingS
|
||||
topPadding: Theme.spacingS
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width - 80 - Theme.spacingL
|
||||
height: 56
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: searchMode === "files" ? "folder" : "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: parentModal ? parentModal.spotlightOpen : true
|
||||
placeholderText: ""
|
||||
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
|
||||
ignoreTabKeys: true
|
||||
keyForwardTargets: [spotlightKeyHandler]
|
||||
onTextChanged: {
|
||||
if (searchMode === "apps") {
|
||||
appLauncher.searchQuery = text
|
||||
}
|
||||
}
|
||||
onTextEdited: {
|
||||
updateSearchMode()
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide()
|
||||
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
|
||||
if (searchMode === "apps") {
|
||||
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
|
||||
appLauncher.launchSelected()
|
||||
else if (appLauncher.model.count > 0)
|
||||
appLauncher.launchApp(appLauncher.model.get(0))
|
||||
} else if (searchMode === "files") {
|
||||
if (fileSearchController.model.count > 0)
|
||||
fileSearchController.openSelected()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key
|
||||
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: searchMode === "apps" && appLauncher.model.count > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "view_list"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "grid_view"
|
||||
size: 18
|
||||
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
appLauncher.setViewMode("grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: searchMode === "files"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: filenameFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "title"
|
||||
size: 18
|
||||
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: filenameFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "filename"
|
||||
}
|
||||
onEntered: {
|
||||
filenameTooltipLoader.active = true
|
||||
Qt.callLater(() => {
|
||||
if (filenameTooltipLoader.item) {
|
||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
onExited: {
|
||||
if (filenameTooltipLoader.item)
|
||||
filenameTooltipLoader.item.hide()
|
||||
|
||||
filenameTooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentFilterButton
|
||||
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "description"
|
||||
size: 18
|
||||
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: contentFilterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
fileSearchController.searchField = "body"
|
||||
}
|
||||
onEntered: {
|
||||
contentTooltipLoader.active = true
|
||||
Qt.callLater(() => {
|
||||
if (contentTooltipLoader.item) {
|
||||
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
|
||||
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
onExited: {
|
||||
if (contentTooltipLoader.item)
|
||||
contentTooltipLoader.item.hide()
|
||||
|
||||
contentTooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
|
||||
SpotlightResults {
|
||||
id: resultsView
|
||||
anchors.fill: parent
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
contextMenu: contextMenu
|
||||
visible: searchMode === "apps"
|
||||
}
|
||||
|
||||
FileSearchResults {
|
||||
id: fileSearchResults
|
||||
anchors.fill: parent
|
||||
fileSearchController: spotlightKeyHandler.fileSearchController
|
||||
visible: searchMode === "files"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: contextMenu.visible
|
||||
z: 999
|
||||
onClicked: () => {
|
||||
contextMenu.hide()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
x: contextMenu.x
|
||||
y: contextMenu.y
|
||||
width: contextMenu.width
|
||||
height: contextMenu.height
|
||||
onClicked: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: filenameTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentTooltipLoader
|
||||
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Popup {
|
||||
id: contextMenu
|
||||
|
||||
property var currentApp: null
|
||||
property var appLauncher: null
|
||||
property var parentHandler: null
|
||||
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
|
||||
|
||||
function show(x, y, app) {
|
||||
currentApp = app
|
||||
contextMenu.x = x + 4
|
||||
contextMenu.y = y + 4
|
||||
contextMenu.open()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
contextMenu.close()
|
||||
}
|
||||
|
||||
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
modal: false
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: pinRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (!desktopEntry)
|
||||
return "push_pin"
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
|
||||
}
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!desktopEntry)
|
||||
return I18n.tr("Pin to Dock")
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (!desktopEntry)
|
||||
return
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || ""
|
||||
if (SessionData.isPinnedApp(appId))
|
||||
SessionData.removePinnedApp(appId)
|
||||
else
|
||||
SessionData.addPinnedApp(appId)
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: actionRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.iconSize - 2
|
||||
height: Theme.iconSize - 2
|
||||
visible: modelData.icon && modelData.icon !== ""
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && desktopEntry) {
|
||||
SessionService.launchDesktopAction(desktopEntry, modelData)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: launchRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "launch"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launchMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (contextMenu.currentApp && appLauncher)
|
||||
appLauncher.launchApp(contextMenu.currentApp)
|
||||
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: SessionService.hasPrimeRun
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
id: primeRunRow
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "memory"
|
||||
size: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Launch on dGPU")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: primeRunMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (desktopEntry) {
|
||||
SessionService.launchDesktopEntry(desktopEntry, true)
|
||||
if (appLauncher && contextMenu.currentApp) {
|
||||
appLauncher.appLaunched(contextMenu.currentApp)
|
||||
}
|
||||
}
|
||||
contextMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
property var appLauncher: null
|
||||
property var contextMenu: null
|
||||
|
||||
function resetScroll() {
|
||||
resultsList.contentY = 0
|
||||
resultsGrid.contentY = 0
|
||||
}
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
property int itemHeight: 60
|
||||
property int iconSize: 40
|
||||
property bool showDescription: true
|
||||
property int itemSpacing: Theme.spacingS
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = index * (itemHeight + itemSpacing)
|
||||
const itemBottom = itemY + itemHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "list"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
clip: true
|
||||
spacing: itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: AppLauncherListDelegate {
|
||||
listView: resultsList
|
||||
itemHeight: resultsList.itemHeight
|
||||
iconSize: resultsList.iconSize
|
||||
showDescription: resultsList.showDescription
|
||||
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
|
||||
keyboardNavigationActive: resultsList.keyboardNavigationActive
|
||||
isCurrentItem: ListView.isCurrentItem
|
||||
iconMaterialSizeAdjustment: 0
|
||||
iconUnicodeScale: 0.8
|
||||
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
|
||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
||||
resultsList.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
||||
}
|
||||
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
|
||||
}
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: resultsGrid
|
||||
|
||||
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
property int columns: 4
|
||||
property bool adaptiveColumns: false
|
||||
property int minCellWidth: 120
|
||||
property int maxCellWidth: 160
|
||||
property int cellPadding: 8
|
||||
property real iconSizeRatio: 0.55
|
||||
property int maxIconSize: 48
|
||||
property int minIconSize: 32
|
||||
property bool hoverUpdatesSelection: false
|
||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
||||
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||
property int baseCellHeight: baseCellWidth + 20
|
||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||
|
||||
signal keyboardNavigationReset
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = Math.floor(index / actualColumns) * cellHeight
|
||||
const itemBottom = itemY + cellHeight
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
clip: true
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
focus: true
|
||||
interactive: true
|
||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||
reuseItems: true
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
}
|
||||
onItemClicked: (index, modelData) => {
|
||||
if (appLauncher)
|
||||
appLauncher.launchApp(modelData)
|
||||
}
|
||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
||||
if (contextMenu)
|
||||
contextMenu.show(mouseX, mouseY, modelData)
|
||||
}
|
||||
onKeyboardNavigationReset: () => {
|
||||
if (appLauncher)
|
||||
appLauncher.keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
delegate: AppLauncherGridDelegate {
|
||||
gridView: resultsGrid
|
||||
cellWidth: resultsGrid.cellWidth
|
||||
cellHeight: resultsGrid.cellHeight
|
||||
cellPadding: resultsGrid.cellPadding
|
||||
minIconSize: resultsGrid.minIconSize
|
||||
maxIconSize: resultsGrid.maxIconSize
|
||||
iconSizeRatio: resultsGrid.iconSizeRatio
|
||||
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
|
||||
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
|
||||
currentIndex: resultsGrid.currentIndex
|
||||
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
|
||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
||||
resultsGrid.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
||||
}
|
||||
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,594 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:wifi-password"
|
||||
|
||||
property string wifiPasswordSSID: ""
|
||||
property string wifiPasswordInput: ""
|
||||
property string wifiUsernameInput: ""
|
||||
property bool requiresEnterprise: false
|
||||
|
||||
property string wifiAnonymousIdentityInput: ""
|
||||
property string wifiDomainInput: ""
|
||||
|
||||
property bool isPromptMode: false
|
||||
property string promptToken: ""
|
||||
property string promptReason: ""
|
||||
property var promptFields: []
|
||||
property string promptSetting: ""
|
||||
|
||||
property bool isVpnPrompt: false
|
||||
property string connectionName: ""
|
||||
property string vpnServiceType: ""
|
||||
property string connectionType: ""
|
||||
|
||||
function show(ssid) {
|
||||
wifiPasswordSSID = ssid
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
isPromptMode = false
|
||||
promptToken = ""
|
||||
promptReason = ""
|
||||
promptFields = []
|
||||
promptSetting = ""
|
||||
isVpnPrompt = false
|
||||
connectionName = ""
|
||||
vpnServiceType = ""
|
||||
connectionType = ""
|
||||
|
||||
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
|
||||
requiresEnterprise = network?.enterprise || false
|
||||
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
||||
contentLoader.item.usernameInput.forceActiveFocus()
|
||||
} else if (contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
|
||||
isPromptMode = true
|
||||
promptToken = token
|
||||
promptReason = reason
|
||||
promptFields = fields || []
|
||||
promptSetting = setting || "802-11-wireless-security"
|
||||
connectionType = connType || "802-11-wireless"
|
||||
connectionName = connName || ssid || ""
|
||||
vpnServiceType = vpnService || ""
|
||||
|
||||
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard")
|
||||
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid
|
||||
|
||||
requiresEnterprise = setting === "802-1x"
|
||||
|
||||
if (reason === "wrong-password") {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
} else {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.text = ""
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
|
||||
contentLoader.item.usernameInput.forceActiveFocus()
|
||||
} else if (contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 420
|
||||
height: requiresEnterprise ? 430 : 230
|
||||
onShouldBeVisibleChanged: () => {
|
||||
if (!shouldBeVisible) {
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
||||
contentLoader.item.usernameInput.forceActiveFocus()
|
||||
} else if (contentLoader.item.passwordInput) {
|
||||
contentLoader.item.passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NetworkService
|
||||
|
||||
function onPasswordDialogShouldReopenChanged() {
|
||||
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
||||
wifiPasswordSSID = NetworkService.connectingSSID
|
||||
wifiPasswordInput = ""
|
||||
open()
|
||||
NetworkService.passwordDialogShouldReopen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: wifiContent
|
||||
|
||||
property alias usernameInput: usernameInput
|
||||
property alias passwordInput: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: event => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (isVpnPrompt) {
|
||||
return I18n.tr("Connect to VPN")
|
||||
}
|
||||
return I18n.tr("Connect to Wi-Fi")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (isVpnPrompt) {
|
||||
return I18n.tr("Enter password for ") + wifiPasswordSSID
|
||||
}
|
||||
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")
|
||||
return prefix + wifiPasswordSSID
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: isPromptMode && promptReason === "wrong-password"
|
||||
text: I18n.tr("Incorrect password")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: usernameInput.activeFocus ? 2 : 1
|
||||
visible: requiresEnterprise && !isVpnPrompt
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
usernameInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: usernameInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiUsernameInput
|
||||
placeholderText: I18n.tr("Username")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiUsernameInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
if (passwordInput) {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: passwordInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiPasswordInput
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
|
||||
backgroundColor: "transparent"
|
||||
focus: !requiresEnterprise
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiPasswordInput = text
|
||||
}
|
||||
onAccepted: () => {
|
||||
if (isPromptMode) {
|
||||
const secrets = {}
|
||||
if (isVpnPrompt) {
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
} else if (promptSetting === "802-11-wireless-security") {
|
||||
secrets["psk"] = passwordInput.text
|
||||
} else if (promptSetting === "802-1x") {
|
||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
||||
}
|
||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
||||
} else {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainInput
|
||||
)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
Component.onCompleted: () => {
|
||||
if (root.shouldBeVisible && !requiresEnterprise)
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: focusDelayTimer
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: () => {
|
||||
if (root.shouldBeVisible) {
|
||||
if (requiresEnterprise && usernameInput) {
|
||||
usernameInput.forceActiveFocus()
|
||||
} else {
|
||||
passwordInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible)
|
||||
focusDelayTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: requiresEnterprise && !isVpnPrompt
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: anonInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
anonInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: anonInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiAnonymousIdentityInput
|
||||
placeholderText: I18n.tr("Anonymous Identity (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiAnonymousIdentityInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: requiresEnterprise && !isVpnPrompt
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: domainMatchInput.activeFocus ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: () => {
|
||||
domainMatchInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: domainMatchInput
|
||||
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: wifiDomainInput
|
||||
placeholderText: I18n.tr("Domain (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.shouldBeVisible
|
||||
onTextEdited: () => {
|
||||
wifiDomainInput = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
|
||||
property bool checked: false
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Theme.outlineButton
|
||||
border.width: 2
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show password")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
if (isPromptMode) {
|
||||
NetworkService.cancelCredentials(promptToken)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: {
|
||||
if (isVpnPrompt) {
|
||||
return passwordInput.text.length > 0
|
||||
}
|
||||
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
|
||||
}
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
StyledText {
|
||||
id: connectText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Connect")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: () => {
|
||||
if (isPromptMode) {
|
||||
const secrets = {}
|
||||
if (isVpnPrompt) {
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
} else if (promptSetting === "802-11-wireless-security") {
|
||||
secrets["psk"] = passwordInput.text
|
||||
} else if (promptSetting === "802-1x") {
|
||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
||||
}
|
||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
||||
} else {
|
||||
const username = requiresEnterprise ? usernameInput.text : ""
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text,
|
||||
username,
|
||||
wifiAnonymousIdentityInput,
|
||||
wifiDomainInput
|
||||
)
|
||||
}
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
wifiUsernameInput = ""
|
||||
wifiAnonymousIdentityInput = ""
|
||||
wifiDomainInput = ""
|
||||
passwordInput.text = ""
|
||||
if (requiresEnterprise) usernameInput.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
Ref {
|
||||
service: DMSNetworkService
|
||||
}
|
||||
|
||||
|
||||
ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
|
||||
ccWidgetPrimaryText: "VPN"
|
||||
ccWidgetSecondaryText: {
|
||||
if (!DMSNetworkService.connected)
|
||||
return "Disconnected"
|
||||
const names = DMSNetworkService.activeNames || []
|
||||
if (names.length <= 1)
|
||||
return names[0] || "Connected"
|
||||
return names[0] + " +" + (names.length - 1)
|
||||
}
|
||||
ccWidgetIsActive: DMSNetworkService.connected
|
||||
|
||||
onCcWidgetToggled: {
|
||||
DMSNetworkService.toggleVpn()
|
||||
}
|
||||
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
id: detailRoot
|
||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
id: detailColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
RowLayout {
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DMSNetworkService.connected)
|
||||
return "Active: None"
|
||||
const names = DMSNetworkService.activeNames || []
|
||||
if (names.length <= 1)
|
||||
return "Active: " + (names[0] || "VPN")
|
||||
return "Active: " + names[0] + " +" + (names.length - 1)
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.width - 120
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 28
|
||||
radius: 14
|
||||
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
||||
visible: DMSNetworkService.connected
|
||||
width: 110
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "link_off"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disconnect")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discAllArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.disconnectAllActive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
width: parent.width
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
width: parent.width
|
||||
height: 160
|
||||
contentHeight: listCol.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: listCol
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
|
||||
visible: height > 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "playlist_remove"
|
||||
size: 36
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No VPN profiles found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Add a VPN in NetworkManager")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DMSNetworkService.profiles
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 300
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
||||
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
|
||||
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.iconSize - 4
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (modelData.type === "wireguard")
|
||||
return "WireGuard"
|
||||
const svc = modelData.serviceType || ""
|
||||
if (svc.indexOf("openvpn") !== -1)
|
||||
return "OpenVPN"
|
||||
if (svc.indexOf("wireguard") !== -1)
|
||||
return "WireGuard (plugin)"
|
||||
if (svc.indexOf("openconnect") !== -1)
|
||||
return "OpenConnect"
|
||||
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1)
|
||||
return "Fortinet"
|
||||
if (svc.indexOf("strongswan") !== -1)
|
||||
return "IPsec (strongSwan)"
|
||||
if (svc.indexOf("libreswan") !== -1)
|
||||
return "IPsec (Libreswan)"
|
||||
if (svc.indexOf("l2tp") !== -1)
|
||||
return "L2TP/IPsec"
|
||||
if (svc.indexOf("pptp") !== -1)
|
||||
return "PPTP"
|
||||
if (svc.indexOf("vpnc") !== -1)
|
||||
return "Cisco (vpnc)"
|
||||
if (svc.indexOf("sstp") !== -1)
|
||||
return "SSTP"
|
||||
if (svc)
|
||||
return svc.split('.').pop()
|
||||
return "VPN"
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.toggle(modelData.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
property bool hasVolumeSliderInCC: {
|
||||
const widgets = SettingsData.controlCenterWidgets || []
|
||||
return widgets.some(widget => widget.id === "volumeSlider")
|
||||
}
|
||||
|
||||
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
id: headerText
|
||||
text: I18n.tr("Audio Devices")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: volumeSlider
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
height: 35
|
||||
spacing: 0
|
||||
visible: !hasVolumeSliderInCC
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
height: Theme.iconSize + Theme.spacingS * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
MouseArea {
|
||||
id: iconArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
|
||||
let muted = AudioService.sink.audio.muted
|
||||
let volume = AudioService.sink.audio.volume
|
||||
if (muted || volume === 0.0) return "volume_off"
|
||||
if (volume <= 0.33) return "volume_down"
|
||||
if (volume <= 0.66) return "volume_up"
|
||||
return "volume_up"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||
enabled: AudioService.sink && AudioService.sink.audio
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
|
||||
showValue: true
|
||||
unit: "%"
|
||||
valueOverride: actualVolumePercent
|
||||
thumbOutlineColor: Theme.surfaceVariant
|
||||
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.volume = newValue / 100
|
||||
if (newValue > 0 && AudioService.sink.audio.muted) {
|
||||
AudioService.sink.audio.muted = false
|
||||
}
|
||||
AudioService.volumeChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: audioContent
|
||||
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
|
||||
contentHeight: audioColumn.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: audioColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && node.isSink && !node.isStream
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset"
|
||||
else if (modelData.name.includes("hdmi"))
|
||||
return "tv"
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset"
|
||||
else
|
||||
return "speaker"
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
Pipewire.preferredDefaultAudioSink = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property string powerOptionsText: I18n.tr("Power Options")
|
||||
readonly property string logOutText: I18n.tr("Log Out")
|
||||
readonly property string suspendText: I18n.tr("Suspend")
|
||||
readonly property string rebootText: I18n.tr("Reboot")
|
||||
readonly property string powerOffText: I18n.tr("Power Off")
|
||||
|
||||
property bool powerMenuVisible: false
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
|
||||
visible: powerMenuVisible
|
||||
implicitWidth: 400
|
||||
implicitHeight: 320
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
||||
height: 320 // Fixed height to prevent cropping
|
||||
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||
y: Theme.barHeight + Theme.spacingXS
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
opacity: powerMenuVisible ? 1 : 0
|
||||
scale: powerMenuVisible ? 1 : 0.85
|
||||
|
||||
MouseArea {
|
||||
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: root.powerOptionsText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 150
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "logout"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.logOutText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"logout", "Log Out",
|
||||
"Are you sure you want to log out?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
||||
Theme.primary.g,
|
||||
Theme.primary.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.suspendText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"suspend", "Suspend",
|
||||
"Are you sure you want to suspend the system?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
|
||||
Theme.warning.g,
|
||||
Theme.warning.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "restart_alt"
|
||||
size: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.rebootText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"reboot", "Reboot",
|
||||
"Are you sure you want to reboot the system?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
|
||||
Theme.error.g,
|
||||
Theme.error.b,
|
||||
0.08) : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "power_settings_new"
|
||||
size: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.powerOffText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false
|
||||
root.powerActionRequested(
|
||||
"poweroff", "Power Off",
|
||||
"Are you sure you want to power off the system?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var barWindow
|
||||
required property var axis
|
||||
required property var appDrawerLoader
|
||||
required property var dankDashPopoutLoader
|
||||
required property var processListPopoutLoader
|
||||
required property var notificationCenterLoader
|
||||
required property var batteryPopoutLoader
|
||||
required property var vpnPopoutLoader
|
||||
required property var controlCenterLoader
|
||||
required property var clipboardHistoryModalPopup
|
||||
required property var systemUpdateLoader
|
||||
required property var notepadInstance
|
||||
|
||||
property alias reveal: core.reveal
|
||||
property alias autoHide: core.autoHide
|
||||
property alias backgroundTransparency: core.backgroundTransparency
|
||||
property alias hasActivePopout: core.hasActivePopout
|
||||
property alias mouseArea: topBarMouseArea
|
||||
|
||||
Item {
|
||||
id: inputMask
|
||||
|
||||
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
|
||||
|
||||
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|
||||
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|
||||
|| !core.autoHide)
|
||||
|
||||
readonly property int maskThickness: showing ? barThickness : 1
|
||||
|
||||
x: {
|
||||
if (!axis.isVertical) {
|
||||
return 0
|
||||
} else {
|
||||
switch (SettingsData.dankBarPosition) {
|
||||
case SettingsData.Position.Left: return 0
|
||||
case SettingsData.Position.Right: return parent.width - maskThickness
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (axis.isVertical) {
|
||||
return 0
|
||||
} else {
|
||||
switch (SettingsData.dankBarPosition) {
|
||||
case SettingsData.Position.Top: return 0
|
||||
case SettingsData.Position.Bottom: return parent.height - maskThickness
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
width: axis.isVertical ? maskThickness : parent.width
|
||||
height: axis.isVertical ? parent.height : maskThickness
|
||||
}
|
||||
|
||||
Region {
|
||||
id: mask
|
||||
item: inputMask
|
||||
}
|
||||
|
||||
property alias maskRegion: mask
|
||||
|
||||
QtObject {
|
||||
id: core
|
||||
|
||||
property real backgroundTransparency: SettingsData.dankBarTransparency
|
||||
property bool autoHide: SettingsData.dankBarAutoHide
|
||||
property bool revealSticky: false
|
||||
|
||||
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
|
||||
|
||||
readonly property bool hasActivePopout: {
|
||||
const loaders = [{
|
||||
"loader": appDrawerLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": dankDashPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": processListPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": notificationCenterLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": batteryPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": vpnPopoutLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": controlCenterLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}, {
|
||||
"loader": clipboardHistoryModalPopup,
|
||||
"prop": "visible"
|
||||
}, {
|
||||
"loader": systemUpdateLoader,
|
||||
"prop": "shouldBeVisible"
|
||||
}]
|
||||
return notepadInstanceVisible || loaders.some(item => {
|
||||
if (item.loader) {
|
||||
return item.loader?.item?.[item.prop]
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
||||
return SettingsData.dankBarOpenOnOverview
|
||||
}
|
||||
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
|
||||
}
|
||||
|
||||
onHasActivePopoutChanged: {
|
||||
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
|
||||
revealSticky = true
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: revealHold
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: core.revealSticky = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDankBarTransparencyChanged() {
|
||||
core.backgroundTransparency = SettingsData.dankBarTransparency
|
||||
}
|
||||
|
||||
target: SettingsData
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: topBarMouseArea
|
||||
function onContainsMouseChanged() {
|
||||
if (topBarMouseArea.containsMouse) {
|
||||
core.revealSticky = true
|
||||
revealHold.stop()
|
||||
} else {
|
||||
if (core.autoHide && !core.hasActivePopout) {
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: topBarMouseArea
|
||||
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
|
||||
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
|
||||
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
||||
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
||||
anchors {
|
||||
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
|
||||
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: SettingsData.dankBarAutoHide && !core.reveal
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var barWindow
|
||||
required property var axis
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
|
||||
anchors.rightMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
|
||||
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
|
||||
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(barWindow.screen)
|
||||
|
||||
function requestRepaint() {
|
||||
debounceTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
barShape.requestPaint()
|
||||
barTint.requestPaint()
|
||||
barBorder.requestPaint()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barShape
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: barWindow
|
||||
function on_BgColorChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceContainerChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
||||
const R = wing
|
||||
const RT = rt
|
||||
const H = H_raw - (R > 0 ? R : 0)
|
||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
function drawTopPath() {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H)
|
||||
|
||||
if (R > 0) {
|
||||
ctx.lineTo(W, H + R)
|
||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
||||
ctx.lineTo(R, H)
|
||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
||||
ctx.lineTo(0, H + R)
|
||||
} else {
|
||||
ctx.lineTo(W, H - RT)
|
||||
ctx.arcTo(W, H, W - RT, H, RT)
|
||||
ctx.lineTo(RT, H)
|
||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
||||
}
|
||||
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
ctx.translate(W, H_raw)
|
||||
ctx.rotate(Math.PI)
|
||||
} else if (isLeft) {
|
||||
ctx.translate(0, W)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
} else if (isRight) {
|
||||
ctx.translate(H_raw, 0)
|
||||
ctx.rotate(Math.PI / 2)
|
||||
}
|
||||
|
||||
drawTopPath()
|
||||
ctx.restore()
|
||||
|
||||
ctx.fillStyle = barWindow._bgColor
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barTint
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
|
||||
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onAlphaTintChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: barWindow
|
||||
function on_BgColorChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
||||
const R = wing
|
||||
const RT = rt
|
||||
const H = H_raw - (R > 0 ? R : 0)
|
||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
function drawTopPath() {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H)
|
||||
|
||||
if (R > 0) {
|
||||
ctx.lineTo(W, H + R)
|
||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
||||
ctx.lineTo(R, H)
|
||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
||||
ctx.lineTo(0, H + R)
|
||||
} else {
|
||||
ctx.lineTo(W, H - RT)
|
||||
ctx.arcTo(W, H, W - RT, H, RT)
|
||||
ctx.lineTo(RT, H)
|
||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
||||
}
|
||||
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
ctx.translate(W, H_raw)
|
||||
ctx.rotate(Math.PI)
|
||||
} else if (isLeft) {
|
||||
ctx.translate(0, W)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
} else if (isRight) {
|
||||
ctx.translate(H_raw, 0)
|
||||
ctx.rotate(Math.PI / 2)
|
||||
}
|
||||
|
||||
drawTopPath()
|
||||
ctx.restore()
|
||||
|
||||
ctx.fillStyle = Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, alphaTint)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: barBorder
|
||||
anchors.fill: parent
|
||||
visible: SettingsData.dankBarBorderEnabled
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
readonly property real correctWidth: Theme.px(root.width, dpr)
|
||||
readonly property real correctHeight: Theme.px(root.height, dpr)
|
||||
canvasSize: Qt.size(correctWidth, correctHeight)
|
||||
|
||||
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
|
||||
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
|
||||
property bool borderEnabled: SettingsData.dankBarBorderEnabled
|
||||
|
||||
antialiasing: rt > 0 || wing > 0
|
||||
|
||||
onWingChanged: root.requestRepaint()
|
||||
onRtChanged: root.requestRepaint()
|
||||
onBorderEnabledChanged: root.requestRepaint()
|
||||
onCorrectWidthChanged: root.requestRepaint()
|
||||
onCorrectHeightChanged: root.requestRepaint()
|
||||
onVisibleChanged: if (visible) root.requestRepaint()
|
||||
Component.onCompleted: root.requestRepaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDprChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Theme
|
||||
function onIsLightModeChanged() { root.requestRepaint() }
|
||||
function onSurfaceTextChanged() { root.requestRepaint() }
|
||||
function onPrimaryChanged() { root.requestRepaint() }
|
||||
function onSecondaryChanged() { root.requestRepaint() }
|
||||
function onOutlineChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarBorderColorChanged() { root.requestRepaint() }
|
||||
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
|
||||
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
|
||||
function onDankBarSpacingChanged() { root.requestRepaint() }
|
||||
function onDankBarSquareCornersChanged() { root.requestRepaint() }
|
||||
function onDankBarTransparencyChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
if (!borderEnabled) return
|
||||
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
|
||||
const R = wing
|
||||
const RT = rt
|
||||
const H = H_raw - (R > 0 ? R : 0)
|
||||
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
|
||||
const spacing = SettingsData.dankBarSpacing
|
||||
const hasEdgeGap = spacing > 0 || RT > 0
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, W, H_raw)
|
||||
|
||||
ctx.save()
|
||||
if (isBottom) {
|
||||
ctx.translate(W, H_raw)
|
||||
ctx.rotate(Math.PI)
|
||||
} else if (isLeft) {
|
||||
ctx.translate(0, W)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
} else if (isRight) {
|
||||
ctx.translate(H_raw, 0)
|
||||
ctx.rotate(Math.PI / 2)
|
||||
}
|
||||
|
||||
const uiThickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
|
||||
const devThickness = Math.max(1, Math.round(Theme.px(uiThickness, dpr)))
|
||||
|
||||
const key = SettingsData.dankBarBorderColor || "surfaceText"
|
||||
const base = (key === "surfaceText") ? Theme.surfaceText
|
||||
: (key === "primary") ? Theme.primary
|
||||
: Theme.secondary
|
||||
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
|
||||
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
ctx.fillStyle = color
|
||||
|
||||
function drawTopBorder() {
|
||||
if (!hasEdgeGap) {
|
||||
ctx.beginPath()
|
||||
ctx.rect(0, H - devThickness, W, devThickness)
|
||||
ctx.fill()
|
||||
} else {
|
||||
const thk = devThickness
|
||||
const RTi = Math.max(0, RT - thk)
|
||||
const Ri = Math.max(0, R - thk)
|
||||
|
||||
ctx.beginPath()
|
||||
|
||||
if (R > 0 && Ri > 0) {
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H)
|
||||
ctx.lineTo(W, H + R)
|
||||
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
|
||||
ctx.lineTo(R, H)
|
||||
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
|
||||
ctx.lineTo(0, H + R)
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
|
||||
ctx.moveTo(RT, thk)
|
||||
ctx.arcTo(thk, thk, thk, RT, RTi)
|
||||
ctx.lineTo(thk, H + R)
|
||||
ctx.arc(R, H + R, Ri, -Math.PI, -Math.PI / 2, false)
|
||||
ctx.lineTo(W - R, H + thk)
|
||||
ctx.arc(W - R, H + R, Ri, -Math.PI / 2, 0, false)
|
||||
ctx.lineTo(W - thk, H + R)
|
||||
ctx.lineTo(W - thk, RT)
|
||||
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
|
||||
ctx.lineTo(RT, thk)
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.moveTo(RT, 0)
|
||||
ctx.lineTo(W - RT, 0)
|
||||
ctx.arcTo(W, 0, W, RT, RT)
|
||||
ctx.lineTo(W, H - RT)
|
||||
ctx.arcTo(W, H, W - RT, H, RT)
|
||||
ctx.lineTo(RT, H)
|
||||
ctx.arcTo(0, H, 0, H - RT, RT)
|
||||
ctx.lineTo(0, RT)
|
||||
ctx.arcTo(0, 0, RT, 0, RT)
|
||||
ctx.closePath()
|
||||
|
||||
ctx.moveTo(RT, thk)
|
||||
ctx.arcTo(thk, thk, thk, RT, RTi)
|
||||
ctx.lineTo(thk, H - RT)
|
||||
ctx.arcTo(thk, H - thk, RT, H - thk, RTi)
|
||||
ctx.lineTo(W - RT, H - thk)
|
||||
ctx.arcTo(W - thk, H - thk, W - thk, H - RT, RTi)
|
||||
ctx.lineTo(W - thk, RT)
|
||||
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
|
||||
ctx.lineTo(RT, thk)
|
||||
ctx.closePath()
|
||||
}
|
||||
|
||||
ctx.fill("evenodd")
|
||||
}
|
||||
}
|
||||
|
||||
drawTopBorder()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,439 +0,0 @@
|
||||
// No external details import; content inlined for consistency
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:vpn"
|
||||
|
||||
Ref {
|
||||
service: DMSNetworkService
|
||||
}
|
||||
|
||||
property bool wasVisible: false
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible && !wasVisible) {
|
||||
DMSNetworkService.getState()
|
||||
}
|
||||
wasVisible = shouldBeVisible
|
||||
}
|
||||
|
||||
property var triggerScreen: null
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x;
|
||||
triggerY = y;
|
||||
triggerWidth = width;
|
||||
triggerSection = section;
|
||||
triggerScreen = screen;
|
||||
}
|
||||
|
||||
popupWidth: 360
|
||||
popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260)
|
||||
triggerX: Screen.width - 380 - Theme.spacingL
|
||||
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
|
||||
triggerWidth: 70
|
||||
positioning: ""
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
visible: shouldBeVisible
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: content
|
||||
|
||||
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
focus: true
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Outer subtle shadow rings to match BatteryPopout
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
color: "transparent"
|
||||
radius: parent.radius + 3
|
||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
||||
border.width: 0
|
||||
z: -3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -2
|
||||
color: "transparent"
|
||||
radius: parent.radius + 2
|
||||
border.color: Theme.shadowMedium
|
||||
border.width: 0
|
||||
z: -2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 0
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("VPN Connections")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Close button (matches BatteryPopout)
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: root.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Inlined VPN details
|
||||
Rectangle {
|
||||
id: vpnDetail
|
||||
|
||||
width: parent.width
|
||||
implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 0
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: detailsColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
RowLayout {
|
||||
spacing: Theme.spacingS
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!DMSNetworkService.connected) {
|
||||
return "Active: None";
|
||||
}
|
||||
|
||||
const names = DMSNetworkService.activeNames || [];
|
||||
if (names.length <= 1) {
|
||||
return "Active: " + (names[0] || "VPN");
|
||||
}
|
||||
|
||||
return "Active: " + names[0] + " +" + (names.length - 1);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: parent.width - 140
|
||||
}
|
||||
|
||||
// Removed Quick Connect for clarity
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
}
|
||||
|
||||
// Disconnect all (shown only when any active)
|
||||
Rectangle {
|
||||
height: 28
|
||||
radius: 14
|
||||
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
|
||||
visible: DMSNetworkService.connected
|
||||
width: 130
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
border.width: 0
|
||||
border.color: Theme.outlineLight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "link_off"
|
||||
size: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Disconnect")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: discAllArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.disconnectAllActive()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1
|
||||
width: parent.width
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
width: parent.width
|
||||
height: 160
|
||||
contentHeight: listCol.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: listCol
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
|
||||
visible: height > 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "playlist_remove"
|
||||
size: 36
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No VPN profiles found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Add a VPN in NetworkManager")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DMSNetworkService.profiles
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 300
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
||||
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
|
||||
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.iconSize - 4
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (modelData.type === "wireguard") {
|
||||
return "WireGuard";
|
||||
}
|
||||
|
||||
const svc = modelData.serviceType || "";
|
||||
if (svc.indexOf("openvpn") !== -1) {
|
||||
return "OpenVPN";
|
||||
}
|
||||
|
||||
if (svc.indexOf("wireguard") !== -1) {
|
||||
return "WireGuard (plugin)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("openconnect") !== -1) {
|
||||
return "OpenConnect";
|
||||
}
|
||||
|
||||
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) {
|
||||
return "Fortinet";
|
||||
}
|
||||
|
||||
if (svc.indexOf("strongswan") !== -1) {
|
||||
return "IPsec (strongSwan)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("libreswan") !== -1) {
|
||||
return "IPsec (Libreswan)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("l2tp") !== -1) {
|
||||
return "L2TP/IPsec";
|
||||
}
|
||||
|
||||
if (svc.indexOf("pptp") !== -1) {
|
||||
return "PPTP";
|
||||
}
|
||||
|
||||
if (svc.indexOf("vpnc") !== -1) {
|
||||
return "Cisco (vpnc)";
|
||||
}
|
||||
|
||||
if (svc.indexOf("sstp") !== -1) {
|
||||
return "SSTP";
|
||||
}
|
||||
|
||||
if (svc) {
|
||||
const parts = svc.split('.');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
return "VPN";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onClicked: DMSNetworkService.toggle(modelData.uuid)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
height: 1
|
||||
width: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property bool hasActiveMedia: activePlayer !== null
|
||||
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
width: 20
|
||||
height: Theme.iconSize
|
||||
|
||||
Loader {
|
||||
active: isPlaying
|
||||
|
||||
sourceComponent: Component {
|
||||
Ref {
|
||||
service: CavaService
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fallbackTimer
|
||||
|
||||
running: !CavaService.cavaAvailable && isPlaying
|
||||
interval: 256
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 1.5
|
||||
|
||||
Repeater {
|
||||
model: 6
|
||||
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: {
|
||||
if (root.isPlaying && CavaService.values.length > index) {
|
||||
const rawLevel = CavaService.values[index] || 0;
|
||||
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
|
||||
const maxHeight = Theme.iconSize - 2;
|
||||
const minHeight = 3;
|
||||
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
radius: 1.5
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.standardDecel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
property bool isActive: false
|
||||
property var popoutTarget: null
|
||||
property var widgetData: null
|
||||
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
|
||||
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
|
||||
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth
|
||||
implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
|
||||
|
||||
Column {
|
||||
id: controlColumn
|
||||
visible: root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return "sync"
|
||||
}
|
||||
|
||||
const status = NetworkService.networkStatus
|
||||
if (status === "ethernet") {
|
||||
return "lan"
|
||||
}
|
||||
|
||||
if (status === "vpn") {
|
||||
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
|
||||
}
|
||||
|
||||
return NetworkService.wifiSignalIcon
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "bluetooth"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: audioIconV.implicitWidth + 4
|
||||
height: audioIconV.implicitHeight + 4
|
||||
color: "transparent"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showAudioIcon
|
||||
|
||||
DankIcon {
|
||||
id: audioIconV
|
||||
|
||||
name: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
||||
return "volume_off"
|
||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
||||
return "volume_down"
|
||||
} else {
|
||||
return "volume_up"
|
||||
}
|
||||
}
|
||||
return "volume_up"
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
let delta = wheelEvent.angleDelta.y
|
||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
|
||||
let newVolume
|
||||
if (delta > 0) {
|
||||
newVolume = Math.min(100, currentVolume + 5)
|
||||
} else {
|
||||
newVolume = Math.max(0, currentVolume - 5)
|
||||
}
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false
|
||||
AudioService.sink.audio.volume = newVolume / 100
|
||||
}
|
||||
wheelEvent.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controlIndicators
|
||||
visible: !root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
id: networkIcon
|
||||
|
||||
name: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return "sync"
|
||||
}
|
||||
|
||||
const status = NetworkService.networkStatus
|
||||
if (status === "ethernet") {
|
||||
return "lan"
|
||||
}
|
||||
|
||||
if (status === "vpn") {
|
||||
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
|
||||
}
|
||||
|
||||
return NetworkService.wifiSignalIcon
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: {
|
||||
if (NetworkService.wifiToggling) {
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: bluetoothIcon
|
||||
|
||||
name: "bluetooth"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: audioIcon.implicitWidth + 4
|
||||
height: audioIcon.implicitHeight + 4
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showAudioIcon
|
||||
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
|
||||
name: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
|
||||
return "volume_off";
|
||||
} else if (AudioService.sink.audio.volume * 100 < 33) {
|
||||
return "volume_down";
|
||||
} else {
|
||||
return "volume_up";
|
||||
}
|
||||
}
|
||||
return "volume_up";
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: audioWheelArea
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
let delta = wheelEvent.angleDelta.y;
|
||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
|
||||
let newVolume;
|
||||
if (delta > 0) {
|
||||
newVolume = Math.min(100, currentVolume + 5);
|
||||
} else {
|
||||
newVolume = Math.max(0, currentVolume - 5);
|
||||
}
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "mic"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: false
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: -root.leftMargin
|
||||
y: -root.topMargin
|
||||
width: root.width + root.leftMargin + root.rightMargin
|
||||
height: root.height + root.topMargin + root.bottomMargin
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
property bool compactMode: SettingsData.keyboardLayoutNameCompactMode
|
||||
property string currentLayout: CompositorService.isNiri ? NiriService.getCurrentKeyboardLayoutName() : ""
|
||||
property string hyprlandKeyboard: ""
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
|
||||
implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
visible: root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: 1
|
||||
|
||||
DankIcon {
|
||||
name: "keyboard"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!root.currentLayout) return ""
|
||||
const parts = root.currentLayout.split(" ")
|
||||
if (parts.length > 0) {
|
||||
return parts[0].substring(0, 2).toUpperCase()
|
||||
}
|
||||
return root.currentLayout.substring(0, 2).toUpperCase()
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
visible: !root.isVerticalOrientation
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: root.currentLayout
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
z: 1
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.cycleKeyboardLayout()
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached([
|
||||
"hyprctl",
|
||||
"switchxkblayout",
|
||||
root.hyprlandKeyboard,
|
||||
"next"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService.isHyprland ? Hyprland : null
|
||||
enabled: CompositorService.isHyprland
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "activelayout") {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (CompositorService.isHyprland) {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
function updateLayout() {
|
||||
if (CompositorService.isHyprland) {
|
||||
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.currentLayout = "Unknown"
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(output)
|
||||
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
||||
root.hyprlandKeyboard = mainKeyboard.name
|
||||
|
||||
if (mainKeyboard) {
|
||||
const layout = mainKeyboard.layout
|
||||
const variant = mainKeyboard.variant
|
||||
const index = mainKeyboard.active_layout_index
|
||||
|
||||
if (root.compactMode && layout && variant && index !== undefined) {
|
||||
const layouts = mainKeyboard.layout.split(",")
|
||||
const variants = mainKeyboard.variant.split(",")
|
||||
const index = mainKeyboard.active_layout_index
|
||||
|
||||
if (layouts[index] && variants[index] !== undefined) {
|
||||
if (variants[index] === "") {
|
||||
root.currentLayout = layouts[index]
|
||||
} else {
|
||||
root.currentLayout = layouts[index] + "-" + variants[index]
|
||||
}
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} else if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||
root.currentLayout = mainKeyboard.active_keymap
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} else {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
} catch (e) {
|
||||
root.currentLayout = "Unknown"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,798 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isVertical: axis?.isVertical ?? false
|
||||
property var axis: null
|
||||
property string section: "left"
|
||||
property var parentScreen
|
||||
property var hoveredItem: null
|
||||
property var topBar: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
|
||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
||||
property int _desktopEntriesUpdateTrigger: 0
|
||||
property int _toplevelsUpdateTrigger: 0
|
||||
|
||||
readonly property var sortedToplevels: {
|
||||
_toplevelsUpdateTrigger
|
||||
const toplevels = CompositorService.sortedToplevels
|
||||
if (!toplevels || toplevels.length === 0) return []
|
||||
|
||||
if (SettingsData.runningAppsCurrentWorkspace) {
|
||||
return CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []
|
||||
}
|
||||
return toplevels
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
_toplevelsUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
_desktopEntriesUpdateTrigger++
|
||||
}
|
||||
}
|
||||
readonly property var groupedWindows: {
|
||||
if (!SettingsData.runningAppsGroupByApp) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
if (!sortedToplevels || sortedToplevels.length === 0) {
|
||||
return []
|
||||
}
|
||||
const appGroups = new Map()
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
if (!toplevel)
|
||||
return
|
||||
const appId = toplevel?.appId || "unknown"
|
||||
if (!appGroups.has(appId)) {
|
||||
appGroups.set(appId, {
|
||||
"appId": appId,
|
||||
"windows": []
|
||||
})
|
||||
}
|
||||
appGroups.get(appId).windows.push({
|
||||
"toplevel": toplevel,
|
||||
"windowId": index,
|
||||
"windowTitle": toplevel?.title || "(Unnamed)"
|
||||
})
|
||||
})
|
||||
return Array.from(appGroups.values())
|
||||
} catch (e) {
|
||||
console.error("RunningApps: groupedWindows error:", e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
readonly property int windowCount: SettingsData.runningAppsGroupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
|
||||
readonly property int calculatedSize: {
|
||||
if (windowCount === 0) {
|
||||
return 0
|
||||
}
|
||||
if (SettingsData.runningAppsCompactMode) {
|
||||
return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
||||
} else {
|
||||
return windowCount * (24 + Theme.spacingXS + 120) + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
|
||||
}
|
||||
}
|
||||
|
||||
width: isVertical ? barThickness : calculatedSize
|
||||
height: isVertical ? calculatedSize : barThickness
|
||||
visible: windowCount > 0
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
width: root.isVertical ? root.widgetThickness : root.calculatedSize
|
||||
height: root.isVertical ? root.calculatedSize : root.widgetThickness
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
clip: false
|
||||
color: {
|
||||
if (windowCount === 0) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const baseColor = Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
property real scrollAccumulator: 0
|
||||
property real touchpadThreshold: 500
|
||||
|
||||
onWheel: wheel => {
|
||||
const deltaY = wheel.angleDelta.y
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
const windows = root.sortedToplevels
|
||||
if (windows.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMouseWheel) {
|
||||
// Direct mouse wheel action
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
if (windows[i].activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let nextIndex
|
||||
if (deltaY < 0) {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = 0
|
||||
} else {
|
||||
nextIndex = (currentIndex + 1) % windows.length
|
||||
}
|
||||
} else {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = windows.length - 1
|
||||
} else {
|
||||
nextIndex = (currentIndex - 1 + windows.length) % windows.length
|
||||
}
|
||||
}
|
||||
|
||||
const nextWindow = windows[nextIndex]
|
||||
if (nextWindow) {
|
||||
nextWindow.activate()
|
||||
}
|
||||
} else {
|
||||
// Touchpad - accumulate small deltas
|
||||
scrollAccumulator += deltaY
|
||||
|
||||
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
if (windows[i].activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let nextIndex
|
||||
if (scrollAccumulator < 0) {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = 0
|
||||
} else {
|
||||
nextIndex = (currentIndex + 1) % windows.length
|
||||
}
|
||||
} else {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = windows.length - 1
|
||||
} else {
|
||||
nextIndex = (currentIndex - 1 + windows.length) % windows.length
|
||||
}
|
||||
}
|
||||
|
||||
const nextWindow = windows[nextIndex]
|
||||
if (nextWindow) {
|
||||
nextWindow.activate()
|
||||
}
|
||||
|
||||
scrollAccumulator = 0
|
||||
}
|
||||
}
|
||||
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: layoutLoader
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowLayout
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
|
||||
property bool isGrouped: SettingsData.runningAppsGroupByApp
|
||||
property var groupData: isGrouped ? modelData : null
|
||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||
property var toplevelObject: toplevelData
|
||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
let appName = "Unknown"
|
||||
if (appId) {
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
|
||||
}
|
||||
if (isGrouped && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)"
|
||||
}
|
||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
||||
}
|
||||
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
||||
|
||||
width: visualWidth
|
||||
height: root.barThickness
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: delegateItem.visualWidth
|
||||
height: 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
} else {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
// App icon
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
||||
return ""
|
||||
}
|
||||
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
return moddedId.toLowerCase().includes("steam_app")
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback text if no icon found
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
return !iconImg.visible && !isSteamApp
|
||||
}
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
if (!appId) {
|
||||
return "?"
|
||||
}
|
||||
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
return appId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
|
||||
anchors.bottomMargin: -2
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: Theme.primary
|
||||
visible: isGrouped && windowCount > 1
|
||||
z: 10
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: windowCount > 9 ? "9+" : windowCount
|
||||
font.pixelSize: 9
|
||||
color: Theme.surface
|
||||
}
|
||||
}
|
||||
|
||||
// Window title text (only visible in expanded mode)
|
||||
StyledText {
|
||||
anchors.left: iconImg.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !SettingsData.runningAppsCompactMode
|
||||
text: windowTitle
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (isGrouped && windowCount > 1) {
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < groupData.windows.length; i++) {
|
||||
if (groupData.windows[i].toplevel.activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const nextIndex = (currentIndex + 1) % groupData.windows.length
|
||||
groupData.windows[nextIndex].toplevel.activate()
|
||||
} else if (toplevelObject) {
|
||||
toplevelObject.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
|
||||
windowContextMenuLoader.active = true
|
||||
if (windowContextMenuLoader.item) {
|
||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
|
||||
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
root.hoveredItem = delegateItem
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
|
||||
const isBottom = root.axis?.edge === "bottom"
|
||||
const tooltipY = isBottom
|
||||
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
|
||||
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (root.hoveredItem === delegateItem) {
|
||||
root.hoveredItem = null
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: columnLayout
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
model: ScriptModel {
|
||||
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
|
||||
property bool isGrouped: SettingsData.runningAppsGroupByApp
|
||||
property var groupData: isGrouped ? modelData : null
|
||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||
property var toplevelObject: toplevelData
|
||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
let appName = "Unknown"
|
||||
if (appId) {
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
|
||||
}
|
||||
if (isGrouped && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)"
|
||||
}
|
||||
return appName + (windowTitle ? " • " + windowTitle : "")
|
||||
}
|
||||
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
|
||||
|
||||
width: root.barThickness
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: delegateItem.visualWidth
|
||||
height: 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
} else {
|
||||
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.barIconSize(root.barThickness)
|
||||
height: Theme.barIconSize(root.barThickness)
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
if (moddedId.toLowerCase().includes("steam_app")) {
|
||||
return ""
|
||||
}
|
||||
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
return moddedId.toLowerCase().includes("steam_app")
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: {
|
||||
const moddedId = Paths.moddedAppId(appId)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
return !iconImg.visible && !isSteamApp
|
||||
}
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger
|
||||
if (!appId) {
|
||||
return "?"
|
||||
}
|
||||
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appId)
|
||||
if (desktopEntry && desktopEntry.name) {
|
||||
return desktopEntry.name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
return appId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
|
||||
anchors.bottomMargin: -2
|
||||
width: 14
|
||||
height: 14
|
||||
radius: 7
|
||||
color: Theme.primary
|
||||
visible: isGrouped && windowCount > 1
|
||||
z: 10
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: windowCount > 9 ? "9+" : windowCount
|
||||
font.pixelSize: 9
|
||||
color: Theme.surface
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: iconImg.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !SettingsData.runningAppsCompactMode
|
||||
text: windowTitle
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (isGrouped && windowCount > 1) {
|
||||
let currentIndex = -1
|
||||
for (var i = 0; i < groupData.windows.length; i++) {
|
||||
if (groupData.windows[i].toplevel.activated) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const nextIndex = (currentIndex + 1) % groupData.windows.length
|
||||
groupData.windows[nextIndex].toplevel.activate()
|
||||
} else if (toplevelObject) {
|
||||
toplevelObject.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
|
||||
windowContextMenuLoader.active = true
|
||||
if (windowContextMenuLoader.item) {
|
||||
windowContextMenuLoader.item.currentWindow = toplevelObject
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
|
||||
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
root.hoveredItem = delegateItem
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
if (root.isVertical) {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
|
||||
const isBottom = root.axis?.edge === "bottom"
|
||||
const tooltipY = isBottom
|
||||
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
|
||||
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (root.hoveredItem === delegateItem) {
|
||||
root.hoveredItem = null
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
|
||||
active: false
|
||||
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: windowContextMenuLoader
|
||||
active: false
|
||||
sourceComponent: PanelWindow {
|
||||
id: contextMenuWindow
|
||||
|
||||
property var currentWindow: null
|
||||
property bool isVisible: false
|
||||
property point anchorPos: Qt.point(0, 0)
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
|
||||
function showAt(x, y, vertical, barEdge) {
|
||||
screen = root.parentScreen
|
||||
anchorPos = Qt.point(x, y)
|
||||
isVertical = vertical ?? false
|
||||
edge = barEdge ?? "top"
|
||||
isVisible = true
|
||||
visible = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
isVisible = false
|
||||
visible = false
|
||||
windowContextMenuLoader.active = false
|
||||
}
|
||||
|
||||
implicitWidth: 100
|
||||
implicitHeight: 40
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: contextMenuWindow.close()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: {
|
||||
if (contextMenuWindow.isVertical) {
|
||||
if (contextMenuWindow.edge === "left") {
|
||||
return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x)
|
||||
} else {
|
||||
return Math.max(10, contextMenuWindow.anchorPos.x - width)
|
||||
}
|
||||
} else {
|
||||
const left = 10
|
||||
const right = contextMenuWindow.width - width - 10
|
||||
const want = contextMenuWindow.anchorPos.x - width / 2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (contextMenuWindow.isVertical) {
|
||||
const top = 10
|
||||
const bottom = contextMenuWindow.height - height - 10
|
||||
const want = contextMenuWindow.anchorPos.y - height / 2
|
||||
return Math.max(top, Math.min(bottom, want))
|
||||
} else {
|
||||
return contextMenuWindow.anchorPos.y
|
||||
}
|
||||
}
|
||||
width: 100
|
||||
height: 32
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Close")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (contextMenuWindow.currentWindow) {
|
||||
contextMenuWindow.currentWindow.close()
|
||||
}
|
||||
contextMenuWindow.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,112 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
Ref {
|
||||
service: DMSNetworkService
|
||||
}
|
||||
|
||||
property var popoutTarget: null
|
||||
property bool isHovered: clickArea.containsMouse
|
||||
|
||||
signal toggleVpnPopup()
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
|
||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||
|
||||
DankIcon {
|
||||
id: icon
|
||||
|
||||
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
|
||||
size: Theme.barIconSize(root.barThickness, -4)
|
||||
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
|
||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: false
|
||||
sourceComponent: DankTooltip {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clickArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
enabled: !DMSNetworkService.isBusy
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.toggleVpnPopup();
|
||||
}
|
||||
onEntered: {
|
||||
if (root.parentScreen && !(popoutTarget && popoutTarget.shouldBeVisible)) {
|
||||
tooltipLoader.active = true
|
||||
if (tooltipLoader.item) {
|
||||
let tooltipText = ""
|
||||
if (!DMSNetworkService.connected) {
|
||||
tooltipText = "VPN Disconnected"
|
||||
} else {
|
||||
const names = DMSNetworkService.activeNames || []
|
||||
if (names.length <= 1) {
|
||||
const name = names[0] || ""
|
||||
const maxLength = 25
|
||||
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
|
||||
tooltipText = "VPN Connected • " + displayName
|
||||
} else {
|
||||
const name = names[0]
|
||||
const maxLength = 20
|
||||
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
|
||||
tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (root.isVerticalOrientation) {
|
||||
const globalPos = mapToGlobal(width / 2, height / 2)
|
||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
||||
const relativeY = globalPos.y - screenY
|
||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
||||
const isLeft = root.axis?.edge === "left"
|
||||
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = mapToGlobal(width / 2, height)
|
||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
||||
tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (tooltipLoader.item) {
|
||||
tooltipLoader.item.hide()
|
||||
}
|
||||
tooltipLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,950 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.I3
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isVertical: axis?.isVertical ?? false
|
||||
property var axis: null
|
||||
property string screenName: ""
|
||||
property real widgetHeight: 30
|
||||
property real barThickness: 48
|
||||
property var hyprlandOverviewLoader: null
|
||||
property var parentScreen: null
|
||||
property int _desktopEntriesUpdateTrigger: 0
|
||||
readonly property var sortedToplevels: {
|
||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
}
|
||||
|
||||
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && ExtWorkspaceService.extWorkspaceAvailable)
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
_desktopEntriesUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
property var currentWorkspace: {
|
||||
if (useExtWorkspace) return getExtWorkspaceActiveWorkspace()
|
||||
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
return getNiriActiveWorkspace()
|
||||
case "hyprland":
|
||||
return getHyprlandActiveWorkspace()
|
||||
case "dwl":
|
||||
const activeTags = getDwlActiveTags()
|
||||
return activeTags.length > 0 ? activeTags[0] : -1
|
||||
case "sway":
|
||||
return getSwayActiveWorkspace()
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
property var dwlActiveTags: {
|
||||
if (CompositorService.isDwl) {
|
||||
return getDwlActiveTags()
|
||||
}
|
||||
return []
|
||||
}
|
||||
property var workspaceList: {
|
||||
if (useExtWorkspace) {
|
||||
const baseList = getExtWorkspaceWorkspaces()
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
|
||||
let baseList
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
baseList = getNiriWorkspaces()
|
||||
break
|
||||
case "hyprland":
|
||||
baseList = getHyprlandWorkspaces()
|
||||
break
|
||||
case "dwl":
|
||||
baseList = getDwlTags()
|
||||
break
|
||||
case "sway":
|
||||
baseList = getSwayWorkspaces()
|
||||
break
|
||||
default:
|
||||
return [1]
|
||||
}
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
|
||||
function getSwayWorkspaces() {
|
||||
const workspaces = I3.workspaces?.values || []
|
||||
if (workspaces.length === 0) return [{"num": 1}]
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return workspaces.slice().sort((a, b) => a.num - b.num)
|
||||
}
|
||||
|
||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
|
||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
|
||||
}
|
||||
|
||||
function getSwayActiveWorkspace() {
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
|
||||
return focusedWs ? focusedWs.num : 1
|
||||
}
|
||||
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true)
|
||||
return focusedWs ? focusedWs.num : 1
|
||||
}
|
||||
|
||||
function getHyprlandWorkspaces() {
|
||||
const workspaces = Hyprland.workspaces?.values || []
|
||||
if (workspaces.length === 0) return [{id: 1}]
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return workspaces.slice().sort((a, b) => a.id - b.id)
|
||||
}
|
||||
|
||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
|
||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [{id: 1}]
|
||||
}
|
||||
|
||||
function getHyprlandActiveWorkspace() {
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return Hyprland.focusedWorkspace?.id || 1
|
||||
}
|
||||
|
||||
const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName)
|
||||
return monitor?.activeWorkspace?.id || 1
|
||||
}
|
||||
|
||||
function getWorkspaceIcons(ws) {
|
||||
_desktopEntriesUpdateTrigger
|
||||
if (!SettingsData.showWorkspaceApps || !ws) {
|
||||
return []
|
||||
}
|
||||
|
||||
let targetWorkspaceId
|
||||
if (CompositorService.isNiri) {
|
||||
const wsNumber = typeof ws === "number" ? ws : -1
|
||||
if (wsNumber <= 0) {
|
||||
return []
|
||||
}
|
||||
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.screenName)
|
||||
if (!workspace) {
|
||||
return []
|
||||
}
|
||||
targetWorkspaceId = workspace.id
|
||||
} else if (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = ws.id !== undefined ? ws.id : ws
|
||||
} else if (CompositorService.isDwl) {
|
||||
if (typeof ws !== "object" || ws.tag === undefined) {
|
||||
return []
|
||||
}
|
||||
targetWorkspaceId = ws.tag
|
||||
} else if (CompositorService.isSway) {
|
||||
targetWorkspaceId = ws.num !== undefined ? ws.num : ws
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
|
||||
|
||||
const byApp = {}
|
||||
let isActiveWs = false
|
||||
if (CompositorService.isNiri) {
|
||||
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active)
|
||||
} else if (CompositorService.isSway) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
|
||||
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false
|
||||
} else if (CompositorService.isDwl) {
|
||||
const output = DwlService.getOutputState(root.screenName)
|
||||
if (output && output.tags) {
|
||||
const tag = output.tags.find(t => t.tag === targetWorkspaceId)
|
||||
isActiveWs = tag ? (tag.state === 1) : false
|
||||
}
|
||||
} else {
|
||||
isActiveWs = targetWorkspaceId === root.currentWorkspace
|
||||
}
|
||||
|
||||
wins.forEach((w, i) => {
|
||||
if (!w) {
|
||||
return
|
||||
}
|
||||
|
||||
let winWs = null
|
||||
if (CompositorService.isNiri) {
|
||||
winWs = w.workspace_id
|
||||
} else if (CompositorService.isSway) {
|
||||
winWs = w.workspace?.num
|
||||
} else {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || [])
|
||||
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w)
|
||||
winWs = hyprToplevel?.workspace?.id
|
||||
}
|
||||
|
||||
|
||||
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
|
||||
return
|
||||
}
|
||||
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown")
|
||||
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
|
||||
|
||||
if (!byApp[key]) {
|
||||
const moddedId = Paths.moddedAppId(keyBase)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId)
|
||||
byApp[key] = {
|
||||
"type": "icon",
|
||||
"icon": icon,
|
||||
"isSteamApp": isSteamApp,
|
||||
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
|
||||
"count": 1,
|
||||
"windowId": w.address || w.id,
|
||||
"fallbackText": w.appId || w.class || w.title || ""
|
||||
}
|
||||
} else {
|
||||
byApp[key].count++
|
||||
if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
|
||||
byApp[key].active = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(byApp)
|
||||
}
|
||||
|
||||
function padWorkspaces(list) {
|
||||
const padded = list.slice()
|
||||
let placeholder
|
||||
if (useExtWorkspace) {
|
||||
placeholder = {"id": "", "name": "", "active": false, "hidden": true}
|
||||
} else if (CompositorService.isHyprland) {
|
||||
placeholder = {"id": -1, "name": ""}
|
||||
} else if (CompositorService.isDwl) {
|
||||
placeholder = {"tag": -1}
|
||||
} else if (CompositorService.isSway) {
|
||||
placeholder = {"num": -1}
|
||||
} else {
|
||||
placeholder = -1
|
||||
}
|
||||
while (padded.length < 3) {
|
||||
padded.push(placeholder)
|
||||
}
|
||||
return padded
|
||||
}
|
||||
|
||||
function getNiriWorkspaces() {
|
||||
if (NiriService.allWorkspaces.length === 0) {
|
||||
return [1, 2]
|
||||
}
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return NiriService.getCurrentOutputWorkspaceNumbers()
|
||||
}
|
||||
|
||||
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1)
|
||||
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
|
||||
}
|
||||
|
||||
function getNiriActiveWorkspace() {
|
||||
if (NiriService.allWorkspaces.length === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return NiriService.getCurrentWorkspaceNumber()
|
||||
}
|
||||
|
||||
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active)
|
||||
return activeWs ? activeWs.idx + 1 : 1
|
||||
}
|
||||
|
||||
function getDwlTags() {
|
||||
if (!DwlService.dwlAvailable) {
|
||||
return []
|
||||
}
|
||||
|
||||
const output = DwlService.getOutputState(root.screenName)
|
||||
if (!output || !output.tags || output.tags.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (SettingsData.dwlShowAllTags) {
|
||||
return output.tags.map(tag => ({"tag": tag.tag, "state": tag.state, "clients": tag.clients, "focused": tag.focused}))
|
||||
}
|
||||
|
||||
const visibleTagIndices = DwlService.getVisibleTags(root.screenName)
|
||||
return visibleTagIndices.map(tagIndex => {
|
||||
const tagData = output.tags.find(t => t.tag === tagIndex)
|
||||
return {
|
||||
"tag": tagIndex,
|
||||
"state": tagData?.state ?? 0,
|
||||
"clients": tagData?.clients ?? 0,
|
||||
"focused": tagData?.focused ?? false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getDwlActiveTags() {
|
||||
if (!DwlService.dwlAvailable) {
|
||||
return []
|
||||
}
|
||||
|
||||
const activeTags = DwlService.getActiveTags(root.screenName)
|
||||
return activeTags
|
||||
}
|
||||
|
||||
function getExtWorkspaceWorkspaces() {
|
||||
const groups = ExtWorkspaceService.groups
|
||||
if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
|
||||
return [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName))
|
||||
if (!group || !group.workspaces) {
|
||||
return [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
const visible = group.workspaces.filter(ws => !ws.hidden).sort((a, b) => {
|
||||
const coordsA = a.coordinates || [0, 0]
|
||||
const coordsB = b.coordinates || [0, 0]
|
||||
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0]
|
||||
return coordsA[1] - coordsB[1]
|
||||
}).map(ws => ({
|
||||
id: ws.id,
|
||||
name: ws.name,
|
||||
coordinates: ws.coordinates,
|
||||
state: ws.state,
|
||||
active: ws.active,
|
||||
urgent: ws.urgent,
|
||||
hidden: ws.hidden,
|
||||
groupID: group.id
|
||||
}))
|
||||
|
||||
return visible.length > 0 ? visible : [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
function getExtWorkspaceActiveWorkspace() {
|
||||
if (!ExtWorkspaceService.extWorkspaceAvailable) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName)
|
||||
return activeWs ? (activeWs.id || activeWs.name || "1") : "1"
|
||||
}
|
||||
|
||||
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
||||
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
|
||||
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
|
||||
|
||||
function getRealWorkspaces() {
|
||||
return root.workspaceList.filter(ws => {
|
||||
if (useExtWorkspace) return ws && ws.id !== "" && !ws.hidden
|
||||
if (CompositorService.isHyprland) return ws && ws.id !== -1
|
||||
if (CompositorService.isDwl) return ws && ws.tag !== -1
|
||||
if (CompositorService.isSway) return ws && ws.num !== -1
|
||||
return ws !== -1
|
||||
})
|
||||
}
|
||||
|
||||
function switchWorkspace(direction) {
|
||||
if (useExtWorkspace) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextWorkspace = realWorkspaces[nextIndex]
|
||||
ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "")
|
||||
} else if (CompositorService.isNiri) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
|
||||
} else if (CompositorService.isDwl) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag)
|
||||
} else if (CompositorService.isSway) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
|
||||
}
|
||||
}
|
||||
|
||||
width: isVertical ? barThickness : visualWidth
|
||||
height: isVertical ? visualHeight : barThickness
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || useExtWorkspace
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
width: root.visualWidth
|
||||
height: root.visualHeight
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground)
|
||||
return "transparent"
|
||||
const baseColor = Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.toggleOverview()
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: workspaceRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.workspaceList
|
||||
}
|
||||
|
||||
Item {
|
||||
id: delegateRoot
|
||||
|
||||
property bool isActive: {
|
||||
if (root.useExtWorkspace) return (modelData?.id || modelData?.name) === root.currentWorkspace
|
||||
if (CompositorService.isHyprland) return !!(modelData && modelData.id === root.currentWorkspace)
|
||||
if (CompositorService.isDwl) return !!(modelData && root.dwlActiveTags.includes(modelData.tag))
|
||||
if (CompositorService.isSway) return !!(modelData && modelData.num === root.currentWorkspace)
|
||||
return modelData === root.currentWorkspace
|
||||
}
|
||||
property bool isPlaceholder: {
|
||||
if (root.useExtWorkspace) return !!(modelData && modelData.hidden)
|
||||
if (CompositorService.isHyprland) return !!(modelData && modelData.id === -1)
|
||||
if (CompositorService.isDwl) return !!(modelData && modelData.tag === -1)
|
||||
if (CompositorService.isSway) return !!(modelData && modelData.num === -1)
|
||||
return modelData === -1
|
||||
}
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
|
||||
property var loadedWorkspaceData: null
|
||||
property bool loadedIsUrgent: false
|
||||
property bool isUrgent: {
|
||||
if (root.useExtWorkspace) return modelData?.urgent ?? false
|
||||
if (CompositorService.isHyprland) return modelData?.urgent ?? false
|
||||
if (CompositorService.isNiri) return loadedIsUrgent
|
||||
if (CompositorService.isDwl) return modelData?.state === 2
|
||||
if (CompositorService.isSway) return loadedIsUrgent
|
||||
return false
|
||||
}
|
||||
property var loadedIconData: null
|
||||
property bool loadedHasIcon: false
|
||||
property var loadedIcons: []
|
||||
|
||||
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
|
||||
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
|
||||
|
||||
readonly property real iconsExtraWidth: {
|
||||
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
||||
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
readonly property real iconsExtraHeight: {
|
||||
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
||||
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property real visualWidth: baseWidth + iconsExtraWidth
|
||||
readonly property real visualHeight: baseHeight + iconsExtraHeight
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: !isPlaceholder
|
||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !isPlaceholder
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (isPlaceholder) return
|
||||
|
||||
const isRightClick = mouse.button === Qt.RightButton
|
||||
|
||||
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
|
||||
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "")
|
||||
} else if (CompositorService.isNiri) {
|
||||
if (isRightClick) {
|
||||
NiriService.toggleOverview()
|
||||
} else {
|
||||
NiriService.switchToWorkspace(modelData - 1)
|
||||
}
|
||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
||||
if (isRightClick && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
||||
} else {
|
||||
Hyprland.dispatch(`workspace ${modelData.id}`)
|
||||
}
|
||||
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
||||
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick)
|
||||
if (isRightClick) {
|
||||
console.log("Calling toggleTag")
|
||||
DwlService.toggleTag(root.screenName, modelData.tag)
|
||||
} else {
|
||||
console.log("Calling switchToTag")
|
||||
DwlService.switchToTag(root.screenName, modelData.tag)
|
||||
}
|
||||
} else if (CompositorService.isSway && modelData?.num) {
|
||||
try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dataUpdateTimer
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
if (isPlaceholder) {
|
||||
delegateRoot.loadedWorkspaceData = null
|
||||
delegateRoot.loadedIconData = null
|
||||
delegateRoot.loadedHasIcon = false
|
||||
delegateRoot.loadedIcons = []
|
||||
delegateRoot.loadedIsUrgent = false
|
||||
return
|
||||
}
|
||||
|
||||
var wsData = null;
|
||||
if (root.useExtWorkspace) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isNiri) {
|
||||
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isDwl) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isSway) {
|
||||
wsData = modelData;
|
||||
}
|
||||
delegateRoot.loadedWorkspaceData = wsData;
|
||||
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
||||
|
||||
var icData = null;
|
||||
if (wsData?.name) {
|
||||
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
|
||||
}
|
||||
delegateRoot.loadedIconData = icData;
|
||||
delegateRoot.loadedHasIcon = icData !== null;
|
||||
|
||||
if (SettingsData.showWorkspaceApps) {
|
||||
if (CompositorService.isDwl || CompositorService.isSway) {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
|
||||
} else {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
|
||||
}
|
||||
} else {
|
||||
delegateRoot.loadedIcons = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllData() {
|
||||
dataUpdateTimer.restart()
|
||||
}
|
||||
|
||||
width: root.isVertical ? root.barThickness : visualWidth
|
||||
height: root.isVertical ? visualHeight : root.barThickness
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: delegateRoot.visualWidth
|
||||
height: delegateRoot.visualHeight
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
||||
|
||||
border.width: isUrgent && !isActive ? 2 : 0
|
||||
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appIconsLoader
|
||||
anchors.fill: parent
|
||||
active: SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
Loader {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowLayout
|
||||
Row {
|
||||
spacing: 4
|
||||
visible: loadedIcons.length > 0
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
}
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
property var windowId: modelData.windowId
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: !modelData.isSteamApp
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: 18
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!appIcon.windowId) return
|
||||
if (CompositorService.isHyprland) {
|
||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(appIcon.windowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.count > 1 && !isActive
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: "black"
|
||||
border.color: "white"
|
||||
border.width: 1
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
z: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.count
|
||||
font.pixelSize: 8
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: columnLayout
|
||||
Column {
|
||||
spacing: 4
|
||||
visible: loadedIcons.length > 0
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
}
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
property var windowId: modelData.windowId
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: !modelData.isSteamApp
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: 18
|
||||
name: "sports_esports"
|
||||
color: Theme.surfaceText
|
||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: modelData.isSteamApp
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!appIcon.windowId) return
|
||||
if (CompositorService.isHyprland) {
|
||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
||||
} else if (CompositorService.isNiri) {
|
||||
NiriService.focusWindow(appIcon.windowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: modelData.count > 1 && !isActive
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: "black"
|
||||
border.color: "white"
|
||||
border.width: 1
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
z: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.count
|
||||
font.pixelSize: 8
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader for Custom Name Icon
|
||||
Loader {
|
||||
id: customIconLoader
|
||||
anchors.fill: parent
|
||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
||||
size: Theme.fontSizeSmall
|
||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
||||
weight: isActive && !isPlaceholder ? 500 : 400
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader for Custom Name Text
|
||||
Loader {
|
||||
id: customTextLoader
|
||||
anchors.fill: parent
|
||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loader for Workspace Index
|
||||
Loader {
|
||||
id: indexLoader
|
||||
anchors.fill: parent
|
||||
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
|
||||
sourceComponent: Item {
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
let isPlaceholder
|
||||
if (root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?.hidden === true
|
||||
} else if (CompositorService.isHyprland) {
|
||||
isPlaceholder = modelData?.id === -1
|
||||
} else if (CompositorService.isDwl) {
|
||||
isPlaceholder = modelData?.tag === -1
|
||||
} else if (CompositorService.isSway) {
|
||||
isPlaceholder = modelData?.num === -1
|
||||
} else {
|
||||
isPlaceholder = modelData === -1
|
||||
}
|
||||
|
||||
if (isPlaceholder) return index + 1
|
||||
|
||||
if (root.useExtWorkspace) return modelData?.name || modelData?.id || ""
|
||||
if (CompositorService.isHyprland) return modelData?.id || ""
|
||||
if (CompositorService.isDwl) return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
|
||||
if (CompositorService.isSway) return modelData?.num || ""
|
||||
return modelData - 1
|
||||
}
|
||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||
font.pixelSize: Theme.barTextSize(barThickness)
|
||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: updateAllData()
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: NiriService
|
||||
enabled: CompositorService.isNiri
|
||||
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
|
||||
function onWindowUrgentChanged() { delegateRoot.updateAllData() }
|
||||
function onWindowsChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
|
||||
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: DwlService
|
||||
enabled: CompositorService.isDwl
|
||||
function onStateChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland.workspaces
|
||||
enabled: CompositorService.isHyprland
|
||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: I3.workspaces
|
||||
enabled: CompositorService.isSway
|
||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: ExtWorkspaceService
|
||||
enabled: root.useExtWorkspace
|
||||
function onStateChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
|
||||
DMSService.addSubscription("extworkspace")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:dash"
|
||||
|
||||
property bool dashVisible: false
|
||||
property var triggerScreen: null
|
||||
property int currentTabIndex: 0
|
||||
|
||||
keyboardFocusMode: WlrKeyboardFocus.Exclusive
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
triggerY = y
|
||||
|
||||
if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)) {
|
||||
const screenWidth = screen ? screen.width : Screen.width
|
||||
triggerX = (screenWidth - popupWidth) / 2
|
||||
triggerWidth = popupWidth
|
||||
} else if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)) {
|
||||
const screenHeight = screen ? screen.height : Screen.height
|
||||
triggerX = (screenHeight - popupHeight) / 2
|
||||
triggerWidth = popupHeight
|
||||
} else {
|
||||
triggerX = x
|
||||
triggerWidth = width
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: 700
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||
triggerX: Screen.width - 620 - Theme.spacingL
|
||||
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
|
||||
triggerWidth: 80
|
||||
shouldBeVisible: dashVisible
|
||||
visible: shouldBeVisible
|
||||
|
||||
property bool __focusArmed: false
|
||||
property bool __contentReady: false
|
||||
|
||||
function __tryFocusOnce() {
|
||||
if (!__focusArmed)
|
||||
return
|
||||
const win = root.window
|
||||
if (!win || !win.visible)
|
||||
return
|
||||
if (!contentLoader.item)
|
||||
return
|
||||
|
||||
if (win.requestActivate)
|
||||
win.requestActivate()
|
||||
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
|
||||
|
||||
if (contentLoader.item.activeFocus)
|
||||
__focusArmed = false
|
||||
}
|
||||
|
||||
onDashVisibleChanged: {
|
||||
if (dashVisible) {
|
||||
__focusArmed = true
|
||||
__contentReady = !!contentLoader.item
|
||||
open()
|
||||
__tryFocusOnce()
|
||||
} else {
|
||||
__focusArmed = false
|
||||
__contentReady = false
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
__contentReady = true
|
||||
if (__focusArmed)
|
||||
__tryFocusOnce()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.window ? root.window : null
|
||||
enabled: !!root.window
|
||||
function onVisibleChanged() {
|
||||
if (__focusArmed)
|
||||
__tryFocusOnce()
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundClicked: {
|
||||
dashVisible = false
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
|
||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
mainContainer.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
Qt.callLater(function () {
|
||||
mainContainer.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dashVisible = false
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
let nextIndex = root.currentTabIndex + 1
|
||||
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
|
||||
nextIndex++
|
||||
}
|
||||
if (nextIndex >= tabBar.model.length) {
|
||||
nextIndex = 0
|
||||
}
|
||||
root.currentTabIndex = nextIndex
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
|
||||
let prevIndex = root.currentTabIndex - 1
|
||||
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
||||
prevIndex--
|
||||
}
|
||||
if (prevIndex < 0) {
|
||||
prevIndex = tabBar.model.length - 1
|
||||
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
|
||||
prevIndex--
|
||||
}
|
||||
}
|
||||
if (prevIndex >= 0) {
|
||||
root.currentTabIndex = prevIndex
|
||||
}
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
|
||||
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
|
||||
if (wallpaperTab.handleKeyEvent(event)) {
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.shouldBeVisible
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.08
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.02
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankTabBar {
|
||||
id: tabBar
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
currentIndex: root.currentTabIndex
|
||||
spacing: Theme.spacingS
|
||||
equalWidthTabs: true
|
||||
enableArrowNavigation: false
|
||||
focus: false
|
||||
activeFocusOnTab: false
|
||||
nextFocusTarget: {
|
||||
const item = pages.currentItem
|
||||
if (!item)
|
||||
return null
|
||||
if (item.focusTarget)
|
||||
return item.focusTarget
|
||||
return item
|
||||
}
|
||||
|
||||
model: {
|
||||
let tabs = [{
|
||||
"icon": "dashboard",
|
||||
"text": I18n.tr("Overview")
|
||||
}, {
|
||||
"icon": "music_note",
|
||||
"text": I18n.tr("Media")
|
||||
}, {
|
||||
"icon": "wallpaper",
|
||||
"text": I18n.tr("Wallpapers")
|
||||
}]
|
||||
|
||||
if (SettingsData.weatherEnabled) {
|
||||
tabs.push({
|
||||
"icon": "wb_sunny",
|
||||
"text": I18n.tr("Weather")
|
||||
})
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
"icon": "settings",
|
||||
"text": I18n.tr("Settings"),
|
||||
"isAction": true
|
||||
})
|
||||
return tabs
|
||||
}
|
||||
|
||||
onTabClicked: function (index) {
|
||||
root.currentTabIndex = index
|
||||
}
|
||||
|
||||
onActionTriggered: function (index) {
|
||||
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
|
||||
if (index === settingsIndex) {
|
||||
dashVisible = false
|
||||
settingsModal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingXS
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: pages
|
||||
width: parent.width
|
||||
implicitHeight: {
|
||||
if (currentIndex === 0)
|
||||
return overviewTab.implicitHeight
|
||||
if (currentIndex === 1)
|
||||
return mediaTab.implicitHeight
|
||||
if (currentIndex === 2)
|
||||
return wallpaperTab.implicitHeight
|
||||
if (SettingsData.weatherEnabled && currentIndex === 3)
|
||||
return weatherTab.implicitHeight
|
||||
return overviewTab.implicitHeight
|
||||
}
|
||||
currentIndex: root.currentTabIndex
|
||||
|
||||
OverviewTab {
|
||||
id: overviewTab
|
||||
|
||||
onCloseDash: {
|
||||
root.dashVisible = false
|
||||
}
|
||||
|
||||
onSwitchToWeatherTab: {
|
||||
if (SettingsData.weatherEnabled) {
|
||||
tabBar.currentIndex = 3
|
||||
tabBar.tabClicked(3)
|
||||
}
|
||||
}
|
||||
|
||||
onSwitchToMediaTab: {
|
||||
tabBar.currentIndex = 1
|
||||
tabBar.tabClicked(1)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayerTab {
|
||||
id: mediaTab
|
||||
}
|
||||
|
||||
WallpaperTab {
|
||||
id: wallpaperTab
|
||||
active: root.currentTabIndex === 2
|
||||
tabBarItem: tabBar
|
||||
keyForwardTarget: mainContainer
|
||||
targetScreen: root.triggerScreen
|
||||
}
|
||||
|
||||
WeatherTab {
|
||||
id: weatherTab
|
||||
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,642 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 700
|
||||
implicitHeight: 410
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
|
||||
|
||||
DankIcon {
|
||||
name: "cloud_off"
|
||||
size: Theme.iconSize * 2
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No Weather Data Available")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 70
|
||||
|
||||
DankIcon {
|
||||
id: refreshButton
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
property bool isRefreshing: false
|
||||
enabled: !isRefreshing
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
onClicked: {
|
||||
refreshButton.isRefreshing = true
|
||||
WeatherService.forceRefresh()
|
||||
refreshTimer.restart()
|
||||
}
|
||||
enabled: parent.enabled
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 2000
|
||||
onTriggered: refreshButton.isRefreshing = false
|
||||
}
|
||||
|
||||
NumberAnimation on rotation {
|
||||
running: refreshButton.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
|
||||
height: 70
|
||||
|
||||
DankIcon {
|
||||
id: weatherIcon
|
||||
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||
size: Theme.iconSize * 1.5
|
||||
color: Theme.primary
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.8
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
||||
shadowOpacity: 0.2
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: tempColumn
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: weatherIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Item {
|
||||
width: tempText.width + unitText.width + Theme.spacingXS
|
||||
height: tempText.height
|
||||
|
||||
StyledText {
|
||||
id: tempText
|
||||
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Light
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: unitText
|
||||
text: SettingsData.useFahrenheit ? "F" : "C"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.left: tempText.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (WeatherService.weather.available) {
|
||||
SettingsData.set("temperatureUnit", !SettingsData.useFahrenheit)
|
||||
}
|
||||
}
|
||||
enabled: WeatherService.weather.available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.city || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: sunriseColumn
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: tempColumn.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
|
||||
|
||||
Item {
|
||||
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
|
||||
height: sunriseIcon.height
|
||||
|
||||
DankIcon {
|
||||
id: sunriseIcon
|
||||
name: "wb_twilight"
|
||||
size: Theme.iconSize - 6
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sunriseText
|
||||
text: WeatherService.weather.sunrise || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: sunriseIcon.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
|
||||
height: sunsetIcon.height
|
||||
|
||||
DankIcon {
|
||||
id: sunsetIcon
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize - 6
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sunsetText
|
||||
text: WeatherService.weather.sunset || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.left: sunsetIcon.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
width: parent.width
|
||||
height: 95
|
||||
columns: 6
|
||||
columnSpacing: Theme.spacingS
|
||||
rowSpacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "device_thermostat"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Feels Like")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "humidity_low"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Humidity")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "air"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Wind")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.wind || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "speed"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Pressure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "rainy"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Rain Chance")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "wb_sunny"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Visibility")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Good")
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("7-Day Forecast")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXS * 6) / 7
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
property var dayDate: {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + index)
|
||||
return date
|
||||
}
|
||||
property bool isToday: index === 0
|
||||
property var forecastData: {
|
||||
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
|
||||
return WeatherService.weather.forecast[index]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||
border.width: isToday ? 1 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isToday ? Theme.primary : Theme.surfaceText
|
||||
font.weight: isToday ? Font.Medium : Font.Normal
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
|
||||
size: Theme.iconSize
|
||||
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: isToday ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: forecastData && forecastData.sunrise && forecastData.sunset
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "wb_twilight"
|
||||
size: 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: forecastData ? forecastData.sunrise : ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: forecastData ? forecastData.sunset : ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Variants {
|
||||
id: dockVariants
|
||||
model: SettingsData.getFilteredScreens("dock")
|
||||
|
||||
property var contextMenu
|
||||
|
||||
delegate: PanelWindow {
|
||||
id: dock
|
||||
|
||||
WlrLayershell.namespace: "dms:dock"
|
||||
|
||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
|
||||
anchors {
|
||||
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
|
||||
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
|
||||
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
|
||||
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
}
|
||||
|
||||
property var modelData: item
|
||||
property bool autoHide: SettingsData.dockAutoHide
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
|
||||
readonly property real widgetHeight: SettingsData.dockIconSize
|
||||
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
|
||||
readonly property real barSpacing: {
|
||||
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
|
||||
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
|
||||
const samePosition = (SettingsData.dockPosition === SettingsData.dankBarPosition)
|
||||
const dockIsHorizontal = !isVertical
|
||||
const dockIsVertical = isVertical
|
||||
|
||||
if (!SettingsData.dankBarVisible) return 0
|
||||
if (dockIsHorizontal && barIsHorizontal && samePosition) {
|
||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
||||
}
|
||||
if (dockIsVertical && barIsVertical && samePosition) {
|
||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property real dockMargin: SettingsData.dockSpacing
|
||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
|
||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
||||
function px(v) { return Math.round(v * _dpr) / _dpr }
|
||||
|
||||
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
||||
property bool revealSticky: false
|
||||
|
||||
Timer {
|
||||
id: revealHold
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: dock.revealSticky = false
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
||||
return true
|
||||
}
|
||||
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
|
||||
}
|
||||
|
||||
onContextMenuOpenChanged: {
|
||||
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
|
||||
revealSticky = true
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDockTransparencyChanged() {
|
||||
dock.backgroundTransparency = SettingsData.dockTransparency
|
||||
}
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: {
|
||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
||||
return SettingsData.dockOpenOnOverview
|
||||
}
|
||||
return SettingsData.showDock
|
||||
}
|
||||
color: "transparent"
|
||||
|
||||
|
||||
exclusiveZone: {
|
||||
if (!SettingsData.showDock || autoHide) return -1
|
||||
if (barSpacing > 0) return -1
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin)
|
||||
}
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
|
||||
Item {
|
||||
id: maskItem
|
||||
parent: dock.contentItem
|
||||
visible: false
|
||||
x: {
|
||||
const baseX = dockCore.x + dockMouseArea.x
|
||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return baseX - animationHeadroom
|
||||
}
|
||||
return baseX
|
||||
}
|
||||
y: {
|
||||
const baseY = dockCore.y + dockMouseArea.y
|
||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return baseY - animationHeadroom
|
||||
}
|
||||
return baseY
|
||||
}
|
||||
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
|
||||
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: maskItem
|
||||
}
|
||||
|
||||
property var hoveredButton: {
|
||||
if (!dockApps.children[0]) {
|
||||
return null
|
||||
}
|
||||
const layoutItem = dockApps.children[0]
|
||||
const flowLayout = layoutItem.children[0]
|
||||
let repeater = null
|
||||
for (var i = 0; i < flowLayout.children.length; i++) {
|
||||
const child = flowLayout.children[i]
|
||||
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
|
||||
repeater = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!repeater || !repeater.itemAt) {
|
||||
return null
|
||||
}
|
||||
for (var i = 0; i < repeater.count; i++) {
|
||||
const item = repeater.itemAt(i)
|
||||
if (item && item.dockButton && item.dockButton.showTooltip) {
|
||||
return item.dockButton
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
DankTooltip {
|
||||
id: dockTooltip
|
||||
targetScreen: dock.screen
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: tooltipRevealDelay
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: dock.showTooltipForHoveredButton()
|
||||
}
|
||||
|
||||
function showTooltipForHoveredButton() {
|
||||
dockTooltip.hide()
|
||||
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
|
||||
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
|
||||
const tooltipText = dock.hoveredButton.tooltipText || ""
|
||||
if (tooltipText) {
|
||||
const screenX = dock.screen ? (dock.screen.x || 0) : 0
|
||||
const screenY = dock.screen ? (dock.screen.y || 0) : 0
|
||||
const screenHeight = dock.screen ? dock.screen.height : 0
|
||||
if (!dock.isVertical) {
|
||||
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
|
||||
const screenRelativeY = isBottom
|
||||
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - SettingsData.dockMargin - 35)
|
||||
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
|
||||
dockTooltip.show(tooltipText,
|
||||
globalX,
|
||||
screenRelativeY,
|
||||
dock.screen,
|
||||
false, false)
|
||||
} else {
|
||||
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + Theme.spacingXS
|
||||
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
|
||||
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
|
||||
dockTooltip.show(tooltipText,
|
||||
screenX + tooltipX,
|
||||
screenRelativeY,
|
||||
dock.screen,
|
||||
isLeft,
|
||||
!isLeft)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dock
|
||||
function onRevealChanged() {
|
||||
if (!dock.reveal) {
|
||||
tooltipRevealDelay.stop()
|
||||
dockTooltip.hide()
|
||||
} else {
|
||||
tooltipRevealDelay.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function onHoveredButtonChanged() {
|
||||
dock.showTooltipForHoveredButton()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockCore
|
||||
anchors.fill: parent
|
||||
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
|
||||
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
|
||||
|
||||
Connections {
|
||||
target: dockMouseArea
|
||||
function onContainsMouseChanged() {
|
||||
if (dockMouseArea.containsMouse) {
|
||||
dock.revealSticky = true
|
||||
revealHold.stop()
|
||||
} else {
|
||||
if (dock.autoHide && !dock.contextMenuOpen) {
|
||||
revealHold.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
property real currentScreen: modelData ? modelData : dock.screen
|
||||
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
|
||||
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
|
||||
property real maxDockWidth: screenWidth * 0.98
|
||||
property real maxDockHeight: screenHeight * 0.98
|
||||
|
||||
height: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
||||
} else {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
|
||||
}
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
|
||||
} else {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
}
|
||||
}
|
||||
anchors {
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockContainer
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
|
||||
transform: Translate {
|
||||
id: dockSlide
|
||||
x: {
|
||||
if (!dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return hideDistance
|
||||
} else {
|
||||
return -hideDistance
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return hideDistance
|
||||
} else {
|
||||
return -hideDistance
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
id: slideXAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
id: slideYAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockBackground
|
||||
objectName: "dockBackground"
|
||||
anchors {
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
||||
overlayColor: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
}
|
||||
}
|
||||
|
||||
DockApps {
|
||||
id: dockApps
|
||||
|
||||
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
|
||||
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
|
||||
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
|
||||
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
|
||||
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
|
||||
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
|
||||
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
|
||||
contextMenu: dockVariants.contextMenu
|
||||
groupByApp: dock.groupByApp
|
||||
isVertical: dock.isVertical
|
||||
dockScreen: dock.screen
|
||||
iconSize: dock.widgetHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isVisible: false
|
||||
property bool showLogout: true
|
||||
property int selectedIndex: 0
|
||||
property int optionCount: {
|
||||
let count = 0
|
||||
if (showLogout) count++
|
||||
count++
|
||||
if (SessionService.hibernateSupported) count++
|
||||
count += 2
|
||||
return count
|
||||
}
|
||||
|
||||
signal closed()
|
||||
|
||||
function show() {
|
||||
isVisible = true
|
||||
selectedIndex = 0
|
||||
Qt.callLater(() => {
|
||||
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
|
||||
powerMenuFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
isVisible = false
|
||||
closed()
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
visible: isVisible
|
||||
z: 1000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: powerMenuFocusScope
|
||||
anchors.fill: parent
|
||||
focus: root.isVisible
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
Qt.callLater(() => forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
root.hide()
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
const actions = []
|
||||
if (showLogout) actions.push("logout")
|
||||
actions.push("suspend")
|
||||
if (SessionService.hibernateSupported) actions.push("hibernate")
|
||||
actions.push("reboot", "poweroff")
|
||||
if (selectedIndex < actions.length) {
|
||||
const action = actions[selectedIndex]
|
||||
hide()
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex + 1) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 320
|
||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
||||
height: implicitHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Options")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 150
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
visible: showLogout
|
||||
color: {
|
||||
if (selectedIndex === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (logoutArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === 0 ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "logout"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Log Out")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const suspendIdx = showLogout ? 1 : 0
|
||||
if (selectedIndex === suspendIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (suspendArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Suspend")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.suspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const hibernateIdx = showLogout ? 2 : 1
|
||||
if (selectedIndex === hibernateIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (hibernateArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
|
||||
visible: SessionService.hibernateSupported
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "ac_unit"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hibernate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hibernateArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.hibernate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let rebootIdx = showLogout ? 3 : 2
|
||||
if (!SessionService.hibernateSupported) rebootIdx--
|
||||
if (selectedIndex === rebootIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (rebootArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: {
|
||||
let rebootIdx = showLogout ? 3 : 2
|
||||
if (!SessionService.hibernateSupported) rebootIdx--
|
||||
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
|
||||
}
|
||||
border.width: {
|
||||
let rebootIdx = showLogout ? 3 : 2
|
||||
if (!SessionService.hibernateSupported) rebootIdx--
|
||||
return selectedIndex === rebootIdx ? 1 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "restart_alt"
|
||||
size: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reboot")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.reboot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let powerOffIdx = showLogout ? 4 : 3
|
||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
||||
if (selectedIndex === powerOffIdx) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
} else if (powerOffArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
}
|
||||
border.color: {
|
||||
let powerOffIdx = showLogout ? 4 : 3
|
||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
||||
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
|
||||
}
|
||||
border.width: {
|
||||
let powerOffIdx = showLogout ? 4 : 3
|
||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
||||
return selectedIndex === powerOffIdx ? 1 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "power_settings_new"
|
||||
size: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Off")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.hide()
|
||||
SessionService.poweroff()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
height: Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankOSD {
|
||||
id: root
|
||||
|
||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
||||
osdHeight: 40 + Theme.spacingS * 2
|
||||
autoHideInterval: 3000
|
||||
enableMouseInteraction: true
|
||||
|
||||
Connections {
|
||||
target: DisplayService
|
||||
function onBrightnessChanged(showOsd) {
|
||||
if (showOsd) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
property int gap: Theme.spacingS
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
|
||||
return "brightness_medium"
|
||||
} else if (deviceInfo.name.includes("kbd")) {
|
||||
return "keyboard"
|
||||
} else {
|
||||
return "lightbulb"
|
||||
}
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: brightnessSlider
|
||||
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
height: 40
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo) return 1
|
||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
||||
if (isExponential) {
|
||||
return 1
|
||||
}
|
||||
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0
|
||||
}
|
||||
maximum: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo) return 100
|
||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
||||
if (isExponential) {
|
||||
return 100
|
||||
}
|
||||
return deviceInfo.displayMax || 100
|
||||
}
|
||||
enabled: DisplayService.brightnessAvailable
|
||||
showValue: true
|
||||
unit: {
|
||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||
if (!deviceInfo) return "%"
|
||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
||||
if (isExponential) {
|
||||
return "%"
|
||||
}
|
||||
return deviceInfo.class === "ddc" ? "" : "%"
|
||||
}
|
||||
thumbOutlineColor: Theme.surfaceContainer
|
||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
||||
|
||||
Component.onCompleted: {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
value = DisplayService.brightnessLevel
|
||||
}
|
||||
}
|
||||
|
||||
onSliderValueChanged: newValue => {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true)
|
||||
resetHideTimer()
|
||||
}
|
||||
}
|
||||
|
||||
onContainsMouseChanged: {
|
||||
setChildHovered(containsMouse)
|
||||
}
|
||||
|
||||
onSliderDragFinished: finalValue => {
|
||||
if (DisplayService.brightnessAvailable) {
|
||||
DisplayService.setBrightness(finalValue, DisplayService.lastIpcDevice, true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DisplayService
|
||||
|
||||
function onBrightnessChanged(showOsd) {
|
||||
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
|
||||
brightnessSlider.value = DisplayService.brightnessLevel
|
||||
}
|
||||
}
|
||||
|
||||
function onDeviceSwitched() {
|
||||
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
|
||||
brightnessSlider.value = DisplayService.brightnessLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankOSD {
|
||||
id: root
|
||||
|
||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
||||
osdHeight: 40 + Theme.spacingS * 2
|
||||
autoHideInterval: 3000
|
||||
enableMouseInteraction: true
|
||||
|
||||
Connections {
|
||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
if (!AudioService.suppressOSD) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
if (!AudioService.suppressOSD) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService
|
||||
|
||||
function onSinkChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
root.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
property int gap: Theme.spacingS
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
|
||||
size: Theme.iconSize
|
||||
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: muteButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
AudioService.toggleMute()
|
||||
}
|
||||
onContainsMouseChanged: {
|
||||
setChildHovered(containsMouse || volumeSlider.containsMouse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: volumeSlider
|
||||
|
||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
||||
readonly property real displayPercent: actualVolumePercent
|
||||
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
height: 40
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
enabled: AudioService.sink && AudioService.sink.audio
|
||||
showValue: true
|
||||
unit: "%"
|
||||
thumbOutlineColor: Theme.surfaceContainer
|
||||
valueOverride: displayPercent
|
||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
||||
|
||||
Component.onCompleted: {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
||||
}
|
||||
}
|
||||
|
||||
onSliderValueChanged: newValue => {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.suppressOSD = true
|
||||
AudioService.sink.audio.volume = newValue / 100
|
||||
AudioService.suppressOSD = false
|
||||
resetHideTimer()
|
||||
}
|
||||
}
|
||||
|
||||
onContainsMouseChanged: {
|
||||
setChildHovered(containsMouse || muteButton.containsMouse)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
if (volumeSlider && !volumeSlider.pressed) {
|
||||
volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOsdShown: {
|
||||
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
|
||||
const slider = contentLoader.item.children[0].children[1]
|
||||
if (slider) {
|
||||
slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property bool isVerticalOrientation: axis?.isVertical ?? false
|
||||
property bool isFirst: false
|
||||
property bool isLast: false
|
||||
property real sectionSpacing: 0
|
||||
property bool isLeftBarEdge: false
|
||||
property bool isRightBarEdge: false
|
||||
property bool isTopBarEdge: false
|
||||
property bool isBottomBarEdge: false
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
||||
readonly property real visualWidth: isVerticalOrientation ? widgetThickness : (contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0)
|
||||
readonly property real visualHeight: isVerticalOrientation ? (contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0) : widgetThickness
|
||||
readonly property alias visualContent: visualContent
|
||||
readonly property real barEdgeExtension: 1000
|
||||
readonly property real gapExtension: sectionSpacing
|
||||
readonly property real leftMargin: !isVerticalOrientation ? (isLeftBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
|
||||
readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
|
||||
readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
|
||||
readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
width: isVerticalOrientation ? barThickness : visualWidth
|
||||
height: isVerticalOrientation ? visualHeight : barThickness
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: root.visualWidth
|
||||
height: root.visualHeight
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const isHovered = mouseArea.containsMouse || (root.isHovered ?? false)
|
||||
const baseColor = isHovered ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
z: -1
|
||||
x: -root.leftMargin
|
||||
y: -root.topMargin
|
||||
width: root.width + root.leftMargin + root.rightMargin
|
||||
height: root.height + root.topMargin + root.bottomMargin
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onPressed: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
return
|
||||
}
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,622 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: dockTab
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
// Dock Position
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockPositionSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: dockPositionSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "swap_vert"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: positionText
|
||||
text: I18n.tr("Dock Position")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - positionText.width - positionButtonGroup.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: positionButtonGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
model: ["Top", "Bottom", "Left", "Right"]
|
||||
currentIndex: {
|
||||
switch (SettingsData.dockPosition) {
|
||||
case SettingsData.Position.Top: return 0
|
||||
case SettingsData.Position.Bottom: return 1
|
||||
case SettingsData.Position.Left: return 2
|
||||
case SettingsData.Position.Right: return 3
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
switch (index) {
|
||||
case 0: SettingsData.setDockPosition(SettingsData.Position.Top); break
|
||||
case 1: SettingsData.setDockPosition(SettingsData.Position.Bottom); break
|
||||
case 2: SettingsData.setDockPosition(SettingsData.Position.Left); break
|
||||
case 3: SettingsData.setDockPosition(SettingsData.Position.Right); break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Visibility Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockVisibilitySection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: dockVisibilitySection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "dock_to_bottom"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- enableToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show Dock")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: enableToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.showDock
|
||||
onToggled: checked => {
|
||||
SettingsData.setShowDock(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: SettingsData.showDock
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
DankIcon {
|
||||
name: "visibility_off"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- autoHideToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Auto-hide Dock")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hide the dock when not in use and reveal it when hovering near the dock area")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: autoHideToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.dockAutoHide
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dockAutoHide", checked)
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: CompositorService.isNiri
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: CompositorService.isNiri
|
||||
|
||||
DankIcon {
|
||||
name: "fullscreen"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- overviewToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show on Overview")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Always show the dock when niri's overview is open")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: overviewToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.dockOpenOnOverview
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dockOpenOnOverview", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group by App
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: groupByAppSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: groupByAppSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "apps"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- groupByAppToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Group by App")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Group multiple windows of the same app together with a window count indicator")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: groupByAppToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.dockGroupByApp
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dockGroupByApp", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indicator Style Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: indicatorStyleSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: indicatorStyleSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "fiber_manual_record"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: indicatorStyleText
|
||||
text: I18n.tr("Indicator Style")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - indicatorStyleText.width - indicatorStyleButtonGroup.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: indicatorStyleButtonGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
model: ["Circle", "Line"]
|
||||
currentIndex: SettingsData.dockIndicatorStyle === "circle" ? 0 : 1
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
SettingsData.set("dockIndicatorStyle", index === 0 ? "circle" : "line")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Size Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: iconSizeSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: iconSizeSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "photo_size_select_large"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Icon Size")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockIconSize
|
||||
minimum: 24
|
||||
maximum: 96
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockIconSize", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Spacing Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockSpacingSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: dockSpacingSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "space_bar"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Spacing")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Padding")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockSpacing
|
||||
minimum: 0
|
||||
maximum: 32
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockSpacing",
|
||||
newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Height to Edge Gap (Exclusive Zone)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockBottomGap
|
||||
minimum: -100
|
||||
maximum: 100
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockBottomGap",
|
||||
newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Margin")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockMargin
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockMargin", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Transparency Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: transparencySection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "opacity"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Dock Transparency")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 32
|
||||
value: Math.round(SettingsData.dockTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockTransparency",
|
||||
newValue / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,669 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property var allPlugins: []
|
||||
property string searchQuery: ""
|
||||
property var filteredPlugins: []
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool isLoading: false
|
||||
property var parentModal: null
|
||||
|
||||
width: 600
|
||||
height: 650
|
||||
allowStacking: true
|
||||
backgroundOpacity: 0
|
||||
closeOnEscapeKey: false
|
||||
|
||||
function updateFilteredPlugins() {
|
||||
var filtered = []
|
||||
var query = searchQuery ? searchQuery.toLowerCase() : ""
|
||||
|
||||
for (var i = 0; i < allPlugins.length; i++) {
|
||||
var plugin = allPlugins[i]
|
||||
var isFirstParty = plugin.firstParty || false
|
||||
|
||||
if (!SessionData.showThirdPartyPlugins && !isFirstParty) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (query.length > 0) {
|
||||
var name = plugin.name ? plugin.name.toLowerCase() : ""
|
||||
var description = plugin.description ? plugin.description.toLowerCase() : ""
|
||||
var author = plugin.author ? plugin.author.toLowerCase() : ""
|
||||
|
||||
if (name.indexOf(query) !== -1 ||
|
||||
description.indexOf(query) !== -1 ||
|
||||
author.indexOf(query) !== -1) {
|
||||
filtered.push(plugin)
|
||||
}
|
||||
} else {
|
||||
filtered.push(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
filteredPlugins = filtered
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredPlugins.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredPlugins.length - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredPlugins.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||
if (selectedIndex === -1) {
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
function installPlugin(pluginName) {
|
||||
ToastService.showInfo("Installing plugin: " + pluginName)
|
||||
DMSService.install(pluginName, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError("Install failed: " + response.error)
|
||||
} else {
|
||||
ToastService.showInfo("Plugin installed: " + pluginName)
|
||||
PluginService.scanPlugins()
|
||||
refreshPlugins()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function refreshPlugins() {
|
||||
isLoading = true
|
||||
DMSService.listPlugins()
|
||||
if (DMSService.apiVersion >= 8) {
|
||||
DMSService.listInstalled()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = false
|
||||
}
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
refreshPlugins()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onDialogClosed: () => {
|
||||
allPlugins = []
|
||||
searchQuery = ""
|
||||
filteredPlugins = []
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
onBackgroundClicked: () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: browserKeyHandler
|
||||
property alias searchField: browserSearchField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
browserSearchField.forceActiveFocus()
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: browserContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
|
||||
Item {
|
||||
id: headerArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height)
|
||||
|
||||
DankIcon {
|
||||
id: headerIcon
|
||||
name: "store"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: headerText
|
||||
text: I18n.tr("Browse Plugins")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.left: headerIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankButton {
|
||||
id: thirdPartyButton
|
||||
text: SessionData.showThirdPartyPlugins ? "Hide 3rd Party" : "Show 3rd Party"
|
||||
iconName: SessionData.showThirdPartyPlugins ? "visibility_off" : "visibility"
|
||||
height: 28
|
||||
onClicked: {
|
||||
if (SessionData.showThirdPartyPlugins) {
|
||||
SessionData.setShowThirdPartyPlugins(false)
|
||||
root.updateFilteredPlugins()
|
||||
} else {
|
||||
thirdPartyConfirmModal.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: refreshButton
|
||||
iconName: "refresh"
|
||||
iconSize: 18
|
||||
iconColor: Theme.primary
|
||||
visible: !root.isLoading
|
||||
onClicked: root.refreshPlugins()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: closeButton
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.outline
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: descriptionText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerArea.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
text: I18n.tr("Install plugins from the DMS plugin registry")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: browserSearchField
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: descriptionText.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
height: 48
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
placeholderText: I18n.tr("Search plugins...")
|
||||
text: root.searchQuery
|
||||
focus: true
|
||||
ignoreLeftRightKeys: true
|
||||
keyForwardTargets: [browserKeyHandler]
|
||||
onTextEdited: {
|
||||
root.searchQuery = text
|
||||
root.updateFilteredPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: listArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: browserSearchField.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.isLoading
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "sync"
|
||||
size: 48
|
||||
color: Theme.primary
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
RotationAnimator on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
running: root.isLoading
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Loading plugins...")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: pluginBrowserList
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
model: ScriptModel {
|
||||
values: root.filteredPlugins
|
||||
}
|
||||
clip: true
|
||||
visible: !root.isLoading
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: browserScrollbar
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: pluginBrowserList.width
|
||||
height: pluginDelegateColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
||||
property bool isInstalled: modelData.installed || false
|
||||
property bool isFirstParty: modelData.firstParty || false
|
||||
color: isSelected ? Theme.primarySelected :
|
||||
Qt.rgba(Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.3)
|
||||
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: isSelected ? 2 : 1
|
||||
|
||||
Column {
|
||||
id: pluginDelegateColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || "extension"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - installButton.width - Theme.spacingM
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 16
|
||||
width: firstPartyText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4)
|
||||
border.width: 1
|
||||
visible: isFirstParty
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: firstPartyText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("official")
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 16
|
||||
width: thirdPartyText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.15)
|
||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.4)
|
||||
border.width: 1
|
||||
visible: !isFirstParty
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: thirdPartyText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("3rd party")
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.warning
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const author = "by " + (modelData.author || "Unknown")
|
||||
const source = modelData.repo ? ` • <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">source</a>` : ""
|
||||
return author + source
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
linkColor: Theme.primary
|
||||
textFormat: Text.RichText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
onLinkActivated: url => Qt.openUrlExternally(url)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: installButton
|
||||
width: 80
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: isInstalled ? Theme.surfaceVariant : Theme.primary
|
||||
opacity: isInstalled ? 1 : (installMouseArea.containsMouse ? 0.9 : 1)
|
||||
border.width: isInstalled ? 1 : 0
|
||||
border.color: Theme.outline
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: isInstalled ? "check" : "download"
|
||||
size: 14
|
||||
color: isInstalled ? Theme.surfaceText : Theme.surface
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: isInstalled ? "Installed" : "Install"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: isInstalled ? Theme.surfaceText : Theme.surface
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: installMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: isInstalled ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !isInstalled
|
||||
onClicked: {
|
||||
if (!isInstalled) {
|
||||
root.installPlugin(modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.description || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: modelData.description && modelData.description.length > 0
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: modelData.capabilities && modelData.capabilities.length > 0
|
||||
|
||||
Repeater {
|
||||
model: modelData.capabilities || []
|
||||
|
||||
Rectangle {
|
||||
height: 18
|
||||
width: capabilityText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 9
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: capabilityText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: listArea
|
||||
text: I18n.tr("No plugins found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: !root.isLoading && root.filteredPlugins.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankModal {
|
||||
id: thirdPartyConfirmModal
|
||||
|
||||
width: 500
|
||||
height: 300
|
||||
allowStacking: true
|
||||
backgroundOpacity: 0.4
|
||||
closeOnEscapeKey: true
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
thirdPartyConfirmModal.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "warning"
|
||||
size: Theme.iconSize
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Third-Party Plugin Warning")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\n\nThese plugins may pose security and privacy risks - install at your own risk.")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• Plugins may contain bugs or security issues")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• Review code before installation when possible")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("• Install only from trusted sources")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - parent.spacing * 3 - y
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Cancel")
|
||||
iconName: "close"
|
||||
onClicked: thirdPartyConfirmModal.close()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("I Understand")
|
||||
iconName: "check"
|
||||
onClicked: {
|
||||
SessionData.setShowThirdPartyPlugins(true)
|
||||
root.updateFilteredPlugins()
|
||||
thirdPartyConfirmModal.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property var allWidgets: []
|
||||
property string targetSection: ""
|
||||
property string searchQuery: ""
|
||||
property var filteredWidgets: []
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property Component widgetSelectionContent
|
||||
property var parentModal: null
|
||||
|
||||
signal widgetSelected(string widgetId, string targetSection)
|
||||
|
||||
function updateFilteredWidgets() {
|
||||
if (!searchQuery || searchQuery.length === 0) {
|
||||
filteredWidgets = allWidgets.slice()
|
||||
return
|
||||
}
|
||||
|
||||
var filtered = []
|
||||
var query = searchQuery.toLowerCase()
|
||||
|
||||
for (var i = 0; i < allWidgets.length; i++) {
|
||||
var widget = allWidgets[i]
|
||||
var text = widget.text ? widget.text.toLowerCase() : ""
|
||||
var description = widget.description ? widget.description.toLowerCase() : ""
|
||||
var id = widget.id ? widget.id.toLowerCase() : ""
|
||||
|
||||
if (text.indexOf(query) !== -1 ||
|
||||
description.indexOf(query) !== -1 ||
|
||||
id.indexOf(query) !== -1) {
|
||||
filtered.push(widget)
|
||||
}
|
||||
}
|
||||
|
||||
filteredWidgets = filtered
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
onAllWidgetsChanged: {
|
||||
updateFilteredWidgets()
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredWidgets.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredWidgets.length === 0) return
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||
if (selectedIndex === -1) {
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectWidget() {
|
||||
if (selectedIndex >= 0 && selectedIndex < filteredWidgets.length) {
|
||||
var widget = filteredWidgets[selectedIndex]
|
||||
root.widgetSelected(widget.id, root.targetSection)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = false
|
||||
}
|
||||
open()
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal && parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
width: 500
|
||||
height: 550
|
||||
allowStacking: true
|
||||
backgroundOpacity: 0
|
||||
closeOnEscapeKey: false
|
||||
onDialogClosed: () => {
|
||||
allWidgets = []
|
||||
targetSection = ""
|
||||
searchQuery = ""
|
||||
filteredWidgets = []
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
if (parentModal) {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal && parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
return hide()
|
||||
}
|
||||
content: widgetSelectionContent
|
||||
|
||||
widgetSelectionContent: Component {
|
||||
FocusScope {
|
||||
id: widgetKeyHandler
|
||||
property alias searchField: searchField
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
root.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (root.keyboardNavigationActive) {
|
||||
root.selectWidget()
|
||||
} else if (root.filteredWidgets.length > 0) {
|
||||
var firstWidget = root.filteredWidgets[0]
|
||||
root.widgetSelected(firstWidget.id, root.targetSection)
|
||||
root.close()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 2
|
||||
iconColor: Theme.outline
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
spacing: Theme.spacingM
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingL + 30 // Space for close button
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "add_circle"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Add Widget to ") + root.targetSection + " Section"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Select a widget to add to the ") + root.targetSection.toLowerCase(
|
||||
) + " section of the top bar. You can add multiple instances of the same widget if needed."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
width: parent.width
|
||||
height: 48
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
placeholderText: ""
|
||||
text: root.searchQuery
|
||||
focus: true
|
||||
ignoreLeftRightKeys: true
|
||||
keyForwardTargets: [widgetKeyHandler]
|
||||
onTextEdited: {
|
||||
root.searchQuery = text
|
||||
updateFilteredWidgets()
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
|
||||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
|
||||
event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: widgetList
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
spacing: Theme.spacingS
|
||||
model: root.filteredWidgets
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
width: widgetList.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
||||
color: isSelected ? Theme.primarySelected :
|
||||
widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(
|
||||
Theme.surfaceVariant.r,
|
||||
Theme.surfaceVariant.g,
|
||||
Theme.surfaceVariant.b,
|
||||
0.3)
|
||||
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: isSelected ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM * 3
|
||||
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.outline
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "add"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: widgetArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.widgetSelected(modelData.id,
|
||||
root.targetSection)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
217
README.md
217
README.md
@@ -1,11 +1,11 @@
|
||||
# DankMaterialShell (dms)
|
||||
# DankMaterialShell
|
||||
|
||||
<div align="center">
|
||||
<a href="https://danklinux.com">
|
||||
<img src="assets/danklogo2.svg" alt="DankMaterialShell Logo" width="200">
|
||||
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
||||
</a>
|
||||
|
||||
### A modern Wayland desktop shell
|
||||
### A modern desktop shell for Wayland
|
||||
|
||||
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||
|
||||
@@ -15,20 +15,33 @@
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||
[](https://ko-fi.com/avengemediallc)
|
||||
[](https://ko-fi.com/danklinux)
|
||||
|
||||
</div>
|
||||
|
||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hypr.land), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop - all in one cohesive package with a gorgeous interface.
|
||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||
|
||||
## Components
|
||||
## Repository Structure
|
||||
|
||||
DankMaterialShell combines two main components:
|
||||
This is a monorepo containing both the shell interface and the core backend services:
|
||||
|
||||
- **[QML/UI Layer](https://github.com/AvengeMedia/DankMaterialShell)** (this repo) - All the visual components, widgets, and shell interface built with Quickshell
|
||||
- **[Go Backend](https://github.com/AvengeMedia/danklinux)** - System integration, IPC, process management, and core services
|
||||
|
||||
---
|
||||
```
|
||||
DankMaterialShell/
|
||||
├── quickshell/ # QML-based shell interface
|
||||
│ ├── Modules/ # UI components (panels, widgets, overlays)
|
||||
│ ├── Services/ # System integration (audio, network, bluetooth)
|
||||
│ ├── Widgets/ # Reusable UI controls
|
||||
│ └── Common/ # Shared resources and themes
|
||||
├── core/ # Go backend and CLI
|
||||
│ ├── cmd/ # dms CLI and dankinstall binaries
|
||||
│ ├── internal/ # System integration, IPC, distro support
|
||||
│ └── pkg/ # Shared packages
|
||||
├── distro/ # Distribution packaging
|
||||
│ ├── fedora/ # Fedora RPM specs
|
||||
│ ├── debian/ # Debian packaging
|
||||
│ └── nix/ # NixOS/home-manager modules
|
||||
└── flake.nix # Nix flake for declarative installation
|
||||
```
|
||||
|
||||
## See it in Action
|
||||
|
||||
@@ -54,166 +67,126 @@ https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Quick Install
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.danklinux.com | sh
|
||||
```
|
||||
|
||||
That's it. One command installs dms and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||
|
||||
**[Manual Installation Guide →](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||
|
||||
---
|
||||
|
||||
## What You Get
|
||||
## Features
|
||||
|
||||
**Dynamic Theming**
|
||||
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (like vscode, vscodium), and more with [matugen](https://github.com/InioX/matugen) and [dank16](https://github.com/AvengeMedia/danklinux/blob/master/internal/dank16/dank16.go).
|
||||
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
|
||||
|
||||
**System Monitoring**
|
||||
Real-time CPU, RAM, GPU metrics and temps with [dgop](https://github.com/AvengeMedia/dgop). Full process list with search and management.
|
||||
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
|
||||
|
||||
**Powerful Launcher**
|
||||
Spotlight-style search for apps, files (via [dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, commands - extensible with plugins.
|
||||
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
|
||||
|
||||
**Control Center**
|
||||
Network, Bluetooth, audio devices, display settings, night mode - all in one clean interface.
|
||||
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
|
||||
|
||||
**Smart Notifications**
|
||||
Notification center with grouping, rich text support, and keyboard navigation.
|
||||
|
||||
**Media Integration**
|
||||
MPRIS player controls, calendar sync, weather widgets, clipboard history with image previews.
|
||||
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||
|
||||
**Complete Session Management**
|
||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, greeter support.
|
||||
**Session Management**
|
||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
||||
|
||||
**Plugin System**
|
||||
Endless customization with the [plugin registry](https://plugins.danklinux.com).
|
||||
|
||||
**TL;DR** - One shell replaces waybar, swaylock, swayidle, mako, fuzzel, polkit and everything else you normally piece together to create a linux desktop.
|
||||
|
||||
---
|
||||
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||
|
||||
## Supported Compositors
|
||||
|
||||
DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)**. - with full workspace switching, overview integration, and monitor management.
|
||||
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||
|
||||
Other Wayland compositors work too, just with a reduced feature set.
|
||||
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||
|
||||
**[Compositor configuration guide →](https://danklinux.com/docs/dankmaterialshell/compositors)**
|
||||
## Command Line Interface
|
||||
|
||||
---
|
||||
|
||||
## Keybinds & IPC
|
||||
|
||||
Control everything from the command line or keybinds:
|
||||
Control the shell from the command line or keybinds:
|
||||
|
||||
```bash
|
||||
dms run # Start the shell
|
||||
dms ipc call spotlight toggle
|
||||
dms ipc call audio setvolume 50
|
||||
dms ipc call wallpaper set /path/to/image.jpg
|
||||
dms ipc call theme toggle
|
||||
dms brightness list # List available displays
|
||||
dms plugins search # Browse plugin registry
|
||||
```
|
||||
|
||||
**[Full keybind and IPC documentation →](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)**
|
||||
|
||||
---
|
||||
|
||||
## Theming
|
||||
|
||||
DankMaterialShell automatically generates color schemes from your wallpaper or theme and applies them to GTK, Qt, terminals, and more.
|
||||
|
||||
DMS is not opinionated or forcing these themes - they are created as optional themes you can enable. You can refer to the documentation if you want to use them:
|
||||
|
||||
**Application theming:** [GTK, Qt, Firefox, terminals, vscode+vscodium →](https://danklinux.com/docs/dankmaterialshell/application-themes)
|
||||
|
||||
**Custom themes:** [Create your own color schemes →](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
||||
|
||||
---
|
||||
|
||||
## Plugins
|
||||
|
||||
Extend dms with the plugin system. Browse community plugins at [plugins.danklinux.com](https://plugins.danklinux.com).
|
||||
|
||||
**[Plugin development guide →](https://danklinux.com/docs/dankmaterialshell/plugins-overview)**
|
||||
|
||||
---
|
||||
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
|
||||
|
||||
## Documentation
|
||||
|
||||
**Website:** [danklinux.com](https://danklinux.com)
|
||||
- **Website:** [danklinux.com](https://danklinux.com)
|
||||
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs)
|
||||
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
||||
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
||||
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||
|
||||
**Docs:** [danklinux.com/docs](https://danklinux.com/docs)
|
||||
## Development
|
||||
|
||||
**Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||
See component-specific documentation:
|
||||
|
||||
---
|
||||
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
|
||||
- **[core/](core/)** - Go backend, CLI tools, and system integration
|
||||
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
|
||||
|
||||
### Building from Source
|
||||
|
||||
**Core + Dankinstall:**
|
||||
```bash
|
||||
cd core
|
||||
make # Build dms CLI
|
||||
make dankinstall # Build installer
|
||||
```
|
||||
|
||||
**Shell:**
|
||||
```bash
|
||||
quickshell -p quickshell/
|
||||
```
|
||||
|
||||
**NixOS:**
|
||||
```nix
|
||||
{
|
||||
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||
|
||||
# Use in home-manager or NixOS configuration
|
||||
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Bug fixes, new widgets, theme improvements, or docs - it all helps.
|
||||
Contributions welcome. Bug fixes, widgets, features, documentation, and plugins all help.
|
||||
|
||||
**Contributing Code:**
|
||||
1. Fork the repository
|
||||
2. Set up the development environment
|
||||
3. Make your changes
|
||||
2. Make your changes
|
||||
3. Test thoroughly
|
||||
4. Open a pull request
|
||||
|
||||
**Contributing Documentation:**
|
||||
1. Fork the [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs) repository
|
||||
2. Update files in the `docs/` folder
|
||||
3. Open a pull request
|
||||
|
||||
### Development Setup
|
||||
|
||||
**Requirements:**
|
||||
- `python3` - Translation management
|
||||
|
||||
**Git Hooks:**
|
||||
|
||||
Enable the pre-commit hook to check translation sync status:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
**Translation Workflow**
|
||||
|
||||
Set POEditor credentials:
|
||||
|
||||
```bash
|
||||
export POEDITOR_API_TOKEN="your_api_token"
|
||||
export POEDITOR_PROJECT_ID="your_project_id"
|
||||
```
|
||||
|
||||
Sync translations before committing:
|
||||
|
||||
```bash
|
||||
python3 scripts/i18nsync.py sync
|
||||
```
|
||||
|
||||
This script:
|
||||
- Extracts strings from QML files
|
||||
- Uploads changed English terms to POEditor
|
||||
- Downloads updated translations from POEditor
|
||||
- Stages all changes for commit
|
||||
|
||||
The pre-commit hook will block commits if translations are out of sync and remind you to run the sync script.
|
||||
|
||||
Without POEditor credentials, the hook is skipped and commits proceed normally.
|
||||
|
||||
Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) or join the community.
|
||||
|
||||
---
|
||||
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs).
|
||||
|
||||
## Credits
|
||||
|
||||
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
||||
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
||||
- [Ly-sec](http://github.com/ly-sec) for awesome wallpaper effects among other things from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
||||
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
||||
- [quickshell](https://quickshell.org/) - Shell framework
|
||||
- [niri](https://github.com/YaLTeR/niri) - Scrolling window manager
|
||||
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
|
||||
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See [LICENSE](LICENSE) for details.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<int> values: Array(6)
|
||||
property int refCount: 0
|
||||
property bool cavaAvailable: false
|
||||
|
||||
Process {
|
||||
id: cavaCheck
|
||||
|
||||
command: ["which", "cava"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.cavaAvailable = exitCode === 0
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cavaCheck.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProcess
|
||||
|
||||
running: root.cavaAvailable && root.refCount > 0
|
||||
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
root.values = Array(6).fill(0)
|
||||
}
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
if (root.refCount > 0 && data.trim()) {
|
||||
let points = data.split(";").map(p => {
|
||||
return parseInt(p.trim(), 10)
|
||||
}).filter(p => {
|
||||
return !isNaN(p)
|
||||
})
|
||||
if (points.length >= 6) {
|
||||
root.values = points.slice(0, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool isHyprland: false
|
||||
property bool isNiri: false
|
||||
property bool isDwl: false
|
||||
property bool isSway: false
|
||||
property bool isLabwc: false
|
||||
property string compositor: "unknown"
|
||||
|
||||
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
||||
readonly property string swaySocket: Quickshell.env("SWAYSOCK")
|
||||
readonly property string labwcPid: Quickshell.env("LABWC_PID")
|
||||
property bool useNiriSorting: isNiri && NiriService
|
||||
|
||||
property var sortedToplevels: []
|
||||
property bool _sortScheduled: false
|
||||
|
||||
signal toplevelsChanged()
|
||||
|
||||
function getScreenScale(screen) {
|
||||
if (!screen) return 1
|
||||
|
||||
if (Quickshell.env("QT_WAYLAND_FORCE_DPI") || Quickshell.env("QT_SCALE_FACTOR")) {
|
||||
return screen.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
if (WlrOutputService.wlrOutputAvailable && screen) {
|
||||
const wlrOutput = WlrOutputService.getOutput(screen.name)
|
||||
if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) {
|
||||
return wlrOutput.scale
|
||||
}
|
||||
}
|
||||
|
||||
if (isNiri && screen) {
|
||||
const niriScale = NiriService.displayScales[screen.name]
|
||||
if (niriScale !== undefined) return niriScale
|
||||
}
|
||||
|
||||
if (isHyprland && screen) {
|
||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
|
||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
||||
}
|
||||
|
||||
if (isDwl && screen) {
|
||||
const dwlScale = DwlService.getOutputScale(screen.name)
|
||||
if (dwlScale !== undefined && dwlScale > 0) return dwlScale
|
||||
}
|
||||
|
||||
return screen?.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: sortDebounceTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
_sortScheduled = false
|
||||
sortedToplevels = computeSortedToplevels()
|
||||
toplevelsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSort() {
|
||||
if (_sortScheduled) return
|
||||
_sortScheduled = true
|
||||
sortDebounceTimer.restart()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ToplevelManager.toplevels
|
||||
function onValuesChanged() { root.scheduleSort() }
|
||||
}
|
||||
Connections {
|
||||
target: isHyprland ? Hyprland : null
|
||||
enabled: isHyprland
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "openwindow" ||
|
||||
event.name === "closewindow" ||
|
||||
event.name === "movewindow" ||
|
||||
event.name === "movewindowv2" ||
|
||||
event.name === "workspace" ||
|
||||
event.name === "workspacev2" ||
|
||||
event.name === "focusedmon" ||
|
||||
event.name === "focusedmonv2" ||
|
||||
event.name === "activewindow" ||
|
||||
event.name === "activewindowv2" ||
|
||||
event.name === "changefloatingmode" ||
|
||||
event.name === "fullscreen" ||
|
||||
event.name === "moveintogroup" ||
|
||||
event.name === "moveoutofgroup") {
|
||||
try {
|
||||
Hyprland.refreshToplevels()
|
||||
} catch(e) {}
|
||||
root.scheduleSort()
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onWindowsChanged() { root.scheduleSort() }
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
detectCompositor()
|
||||
scheduleSort()
|
||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DwlService
|
||||
function onStateChanged() {
|
||||
if (isDwl && !isHyprland && !isNiri) {
|
||||
scheduleSort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeSortedToplevels() {
|
||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
|
||||
return []
|
||||
|
||||
if (useNiriSorting)
|
||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
||||
|
||||
if (isHyprland)
|
||||
return sortHyprlandToplevelsSafe()
|
||||
|
||||
return Array.from(ToplevelManager.toplevels.values)
|
||||
}
|
||||
|
||||
function _get(o, path, fallback) {
|
||||
try {
|
||||
let v = o
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
if (v === null || v === undefined) return fallback
|
||||
v = v[path[i]]
|
||||
}
|
||||
return (v === undefined || v === null) ? fallback : v
|
||||
} catch (e) { return fallback }
|
||||
}
|
||||
|
||||
function sortHyprlandToplevelsSafe() {
|
||||
if (!Hyprland.toplevels || !Hyprland.toplevels.values) return []
|
||||
|
||||
const items = Array.from(Hyprland.toplevels.values)
|
||||
|
||||
function _get(o, path, fb) {
|
||||
try {
|
||||
let v = o
|
||||
for (let k of path) { if (v == null) return fb; v = v[k] }
|
||||
return (v == null) ? fb : v
|
||||
} catch(e) { return fb }
|
||||
}
|
||||
|
||||
let snap = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const t = items[i]
|
||||
if (!t) continue
|
||||
|
||||
const addr = t.address || ""
|
||||
if (!addr) continue
|
||||
|
||||
const li = t.lastIpcObject || null
|
||||
|
||||
const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], "")
|
||||
const monX = _get(t, ["monitor", "x"], Number.MAX_SAFE_INTEGER)
|
||||
const monY = _get(t, ["monitor", "y"], Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const at = _get(li, ["at"], null)
|
||||
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : 1e9
|
||||
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : 1e9
|
||||
|
||||
const relX = Number.isFinite(monX) ? (atX - monX) : atX
|
||||
const relY = Number.isFinite(monY) ? (atY - monY) : atY
|
||||
|
||||
snap.push({
|
||||
monKey: String(monName),
|
||||
monOrderX: Number.isFinite(monX) ? monX : Number.MAX_SAFE_INTEGER,
|
||||
monOrderY: Number.isFinite(monY) ? monY : Number.MAX_SAFE_INTEGER,
|
||||
wsId: (typeof wsId === "number") ? wsId : Number.MAX_SAFE_INTEGER,
|
||||
x: relX,
|
||||
y: relY,
|
||||
title: t.title || "",
|
||||
address: addr,
|
||||
wayland: t.wayland
|
||||
})
|
||||
}
|
||||
|
||||
const groups = new Map()
|
||||
for (const it of snap) {
|
||||
const key = it.monKey + "::" + it.wsId
|
||||
if (!groups.has(key)) groups.set(key, [])
|
||||
groups.get(key).push(it)
|
||||
}
|
||||
|
||||
let groupList = []
|
||||
for (const [key, arr] of groups) {
|
||||
const repr = arr[0]
|
||||
groupList.push({
|
||||
key,
|
||||
monKey: repr.monKey,
|
||||
monOrderX: repr.monOrderX,
|
||||
monOrderY: repr.monOrderY,
|
||||
wsId: repr.wsId,
|
||||
items: arr
|
||||
})
|
||||
}
|
||||
|
||||
groupList.sort((a, b) => {
|
||||
if (a.monOrderX !== b.monOrderX) return a.monOrderX - b.monOrderX
|
||||
if (a.monOrderY !== b.monOrderY) return a.monOrderY - b.monOrderY
|
||||
if (a.monKey !== b.monKey) return a.monKey.localeCompare(b.monKey)
|
||||
if (a.wsId !== b.wsId) return a.wsId - b.wsId
|
||||
return 0
|
||||
})
|
||||
|
||||
const COLUMN_THRESHOLD = 48
|
||||
const JITTER_Y = 6
|
||||
|
||||
let ordered = []
|
||||
for (const g of groupList) {
|
||||
const arr = g.items
|
||||
|
||||
const xs = arr.map(it => it.x).filter(x => Number.isFinite(x)).sort((a, b) => a - b)
|
||||
let colCenters = []
|
||||
if (xs.length > 0) {
|
||||
for (const x of xs) {
|
||||
if (colCenters.length === 0) {
|
||||
colCenters.push(x)
|
||||
} else {
|
||||
const last = colCenters[colCenters.length - 1]
|
||||
if (x - last >= COLUMN_THRESHOLD) {
|
||||
colCenters.push(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
colCenters = [0]
|
||||
}
|
||||
|
||||
for (const it of arr) {
|
||||
let bestCol = 0
|
||||
let bestDist = Number.POSITIVE_INFINITY
|
||||
for (let ci = 0; ci < colCenters.length; ci++) {
|
||||
const d = Math.abs(it.x - colCenters[ci])
|
||||
if (d < bestDist) {
|
||||
bestDist = d
|
||||
bestCol = ci
|
||||
}
|
||||
}
|
||||
it._col = bestCol
|
||||
}
|
||||
|
||||
arr.sort((a, b) => {
|
||||
if (a._col !== b._col) return a._col - b._col
|
||||
|
||||
const dy = a.y - b.y
|
||||
if (Math.abs(dy) > JITTER_Y) return dy
|
||||
|
||||
if (a.title !== b.title) return a.title.localeCompare(b.title)
|
||||
if (a.address !== b.address) return a.address.localeCompare(b.address)
|
||||
return 0
|
||||
})
|
||||
|
||||
ordered.push.apply(ordered, arr)
|
||||
}
|
||||
|
||||
return ordered.map(x => x.wayland).filter(w => w !== null && w !== undefined)
|
||||
}
|
||||
|
||||
function filterCurrentWorkspace(toplevels, screen) {
|
||||
if (useNiriSorting) return NiriService.filterCurrentWorkspace(toplevels, screen)
|
||||
if (isHyprland) return filterHyprlandCurrentWorkspaceSafe(toplevels, screen)
|
||||
return toplevels
|
||||
}
|
||||
|
||||
function filterHyprlandCurrentWorkspaceSafe(toplevels, screenName) {
|
||||
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) return toplevels
|
||||
|
||||
let currentWorkspaceId = null
|
||||
try {
|
||||
const hy = Array.from(Hyprland.toplevels.values)
|
||||
for (const t of hy) {
|
||||
const mon = _get(t, ["monitor", "name"], "")
|
||||
const wsId = _get(t, ["workspace", "id"], null)
|
||||
const active = !!_get(t, ["activated"], false)
|
||||
if (mon === screenName && wsId !== null) {
|
||||
if (active) { currentWorkspaceId = wsId; break }
|
||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null && Hyprland.workspaces) {
|
||||
const wss = Array.from(Hyprland.workspaces.values)
|
||||
const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null)
|
||||
for (const ws of wss) {
|
||||
const monName = _get(ws, ["monitor"], "")
|
||||
const wsId = _get(ws, ["id"], null)
|
||||
if (monName === screenName && wsId !== null) {
|
||||
if (focusedId !== null && wsId === focusedId) { currentWorkspaceId = wsId; break }
|
||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CompositorService: workspace snapshot failed:", e)
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null) return toplevels
|
||||
|
||||
// Map wayland → wsId snapshot
|
||||
let map = new Map()
|
||||
try {
|
||||
const hy = Array.from(Hyprland.toplevels.values)
|
||||
for (const t of hy) {
|
||||
const wsId = _get(t, ["workspace", "id"], null)
|
||||
if (t && t.wayland && wsId !== null) map.set(t.wayland, wsId)
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return toplevels.filter(w => map.get(w) === currentWorkspaceId)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: compositorInitTimer
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
detectCompositor()
|
||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
||||
}
|
||||
}
|
||||
|
||||
function detectCompositor() {
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0 &&
|
||||
!niriSocket && !swaySocket && !labwcPid) {
|
||||
isHyprland = true
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "hyprland"
|
||||
console.info("CompositorService: Detected Hyprland")
|
||||
return
|
||||
}
|
||||
|
||||
if (niriSocket && niriSocket.length > 0) {
|
||||
Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
isNiri = true
|
||||
isHyprland = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "niri"
|
||||
console.info("CompositorService: Detected Niri with socket:", niriSocket)
|
||||
NiriService.generateNiriBinds()
|
||||
NiriService.generateNiriBlurrule()
|
||||
}
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (swaySocket && swaySocket.length > 0) {
|
||||
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
isNiri = false
|
||||
isHyprland = false
|
||||
isDwl = false
|
||||
isSway = true
|
||||
isLabwc = false
|
||||
compositor = "sway"
|
||||
console.info("CompositorService: Detected Sway with socket:", swaySocket)
|
||||
}
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (labwcPid && labwcPid.length > 0) {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = true
|
||||
compositor = "labwc"
|
||||
console.info("CompositorService: Detected LabWC with PID:", labwcPid)
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.dmsAvailable) {
|
||||
Qt.callLater(checkForDwl)
|
||||
} else {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "unknown"
|
||||
console.warn("CompositorService: No compositor detected")
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
if (!isHyprland && !isNiri && !isDwl && !isLabwc) {
|
||||
checkForDwl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkForDwl() {
|
||||
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = true
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "dwl"
|
||||
console.info("CompositorService: Detected DWL via DMS capability")
|
||||
}
|
||||
}
|
||||
|
||||
function powerOffMonitors() {
|
||||
if (isNiri) return NiriService.powerOffMonitors()
|
||||
if (isHyprland) return Hyprland.dispatch("dpms off")
|
||||
if (isDwl) return _dwlPowerOffMonitors()
|
||||
if (isSway) { try { I3.dispatch("output * dpms off") } catch(_){} return }
|
||||
console.warn("CompositorService: Cannot power off monitors, unknown compositor")
|
||||
}
|
||||
|
||||
function powerOnMonitors() {
|
||||
if (isNiri) return NiriService.powerOnMonitors()
|
||||
if (isHyprland) return Hyprland.dispatch("dpms on")
|
||||
if (isDwl) return _dwlPowerOnMonitors()
|
||||
if (isSway) { try { I3.dispatch("output * dpms on") } catch(_){} return }
|
||||
console.warn("CompositorService: Cannot power on monitors, unknown compositor")
|
||||
}
|
||||
|
||||
function _dwlPowerOffMonitors() {
|
||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
||||
console.warn("CompositorService: No screens available for DWL power off")
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
const screen = Quickshell.screens[i]
|
||||
if (screen && screen.name) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _dwlPowerOnMonitors() {
|
||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
||||
console.warn("CompositorService: No screens available for DWL power on")
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
const screen = Quickshell.screens[i]
|
||||
if (screen && screen.name) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
|
||||
property var printerNames: []
|
||||
property var printers: []
|
||||
property string selectedPrinter: ""
|
||||
|
||||
property bool cupsAvailable: false
|
||||
property bool stateInitialized: false
|
||||
|
||||
signal cupsStateUpdate
|
||||
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: DMSService.isConnected
|
||||
|
||||
function onCupsStateUpdate(data) {
|
||||
console.log("CupsService: Subscription update received")
|
||||
getState()
|
||||
}
|
||||
|
||||
function onCapabilitiesChanged() {
|
||||
checkDMSCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
function checkDMSCapabilities() {
|
||||
if (!DMSService.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.capabilities.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
cupsAvailable = DMSService.capabilities.includes("cups")
|
||||
|
||||
if (cupsAvailable && !stateInitialized) {
|
||||
stateInitialized = true
|
||||
getState()
|
||||
}
|
||||
}
|
||||
|
||||
function getState() {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
DMSService.sendRequest("cups.getPrinters", null, response => {
|
||||
if (response.result) {
|
||||
updatePrinters(response.result)
|
||||
fetchAllJobs()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updatePrinters(printersData) {
|
||||
printerNames = printersData.map(p => p.name)
|
||||
|
||||
let printersObj = {}
|
||||
for (var i = 0; i < printersData.length; i++) {
|
||||
let printer = printersData[i]
|
||||
printersObj[printer.name] = {
|
||||
"state": printer.state,
|
||||
"stateReason": printer.stateReason,
|
||||
"jobs": []
|
||||
}
|
||||
}
|
||||
printers = printersObj
|
||||
|
||||
if (printerNames.length > 0) {
|
||||
if (selectedPrinter.length > 0) {
|
||||
if (!printerNames.includes(selectedPrinter)) {
|
||||
selectedPrinter = printerNames[0]
|
||||
}
|
||||
} else {
|
||||
selectedPrinter = printerNames[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAllJobs() {
|
||||
for (var i = 0; i < printerNames.length; i++) {
|
||||
fetchJobsForPrinter(printerNames[i])
|
||||
}
|
||||
}
|
||||
|
||||
function fetchJobsForPrinter(printerName) {
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.getJobs", params, response => {
|
||||
if (response.result && printers[printerName]) {
|
||||
let updatedPrinters = Object.assign({}, printers)
|
||||
updatedPrinters[printerName].jobs = response.result
|
||||
printers = updatedPrinters
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getSelectedPrinter() {
|
||||
return selectedPrinter
|
||||
}
|
||||
|
||||
function setSelectedPrinter(printerName) {
|
||||
if (printerNames.length > 0) {
|
||||
if (printerNames.includes(printerName)) {
|
||||
selectedPrinter = printerName
|
||||
} else {
|
||||
selectedPrinter = printerNames[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPrintersNum() {
|
||||
if (!cupsAvailable)
|
||||
return 0
|
||||
|
||||
return printerNames.length
|
||||
}
|
||||
|
||||
function getPrintersNames() {
|
||||
if (!cupsAvailable)
|
||||
return []
|
||||
|
||||
return printerNames
|
||||
}
|
||||
|
||||
function getTotalJobsNum() {
|
||||
if (!cupsAvailable)
|
||||
return 0
|
||||
|
||||
var result = 0
|
||||
for (var i = 0; i < printerNames.length; i++) {
|
||||
var printerName = printerNames[i]
|
||||
if (printers[printerName] && printers[printerName].jobs) {
|
||||
result += printers[printerName].jobs.length
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getCurrentPrinterState() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return ""
|
||||
|
||||
var printer = printers[selectedPrinter]
|
||||
return printer.state
|
||||
}
|
||||
|
||||
function getCurrentPrinterStatePrettyShort() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return ""
|
||||
|
||||
var printer = printers[selectedPrinter]
|
||||
return getPrinterStateTranslation(printer.state) + " (" + getPrinterStateReasonTranslation(printer.stateReason) + ")"
|
||||
}
|
||||
|
||||
function getCurrentPrinterStatePretty() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return ""
|
||||
|
||||
var printer = printers[selectedPrinter]
|
||||
return getPrinterStateTranslation(printer.state) + " (" + I18n.tr("Reason") + ": " + getPrinterStateReasonTranslation(printer.stateReason) + ")"
|
||||
}
|
||||
|
||||
function getCurrentPrinterJobs() {
|
||||
if (!cupsAvailable || !selectedPrinter)
|
||||
return []
|
||||
|
||||
return getJobs(selectedPrinter)
|
||||
}
|
||||
|
||||
function getJobs(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return ""
|
||||
|
||||
var printer = printers[printerName]
|
||||
return printer.jobs
|
||||
}
|
||||
|
||||
function getJobsNum(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return 0
|
||||
|
||||
var printer = printers[printerName]
|
||||
return printer.jobs.length
|
||||
}
|
||||
|
||||
function pausePrinter(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.pausePrinter", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to pause printer") + " - " + response.error)
|
||||
} else {
|
||||
getState()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resumePrinter(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.resumePrinter", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to resume printer") + " - " + response.error)
|
||||
} else {
|
||||
getState()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cancelJob(printerName, jobID) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName,
|
||||
"jobID": jobID
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.cancelJob", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to cancel selected job") + " - " + response.error)
|
||||
} else {
|
||||
fetchJobsForPrinter(printerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function purgeJobs(printerName) {
|
||||
if (!cupsAvailable)
|
||||
return
|
||||
|
||||
const params = {
|
||||
"printerName": printerName
|
||||
}
|
||||
|
||||
DMSService.sendRequest("cups.purgeJobs", params, response => {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to cancel all jobs") + " - " + response.error)
|
||||
} else {
|
||||
fetchJobsForPrinter(printerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
readonly property var states: ({
|
||||
"idle": I18n.tr("Idle"),
|
||||
"processing": I18n.tr("Processing"),
|
||||
"stopped": I18n.tr("Stopped")
|
||||
})
|
||||
|
||||
readonly property var reasonsGeneral: ({
|
||||
"none": I18n.tr("None"),
|
||||
"other": I18n.tr("Other")
|
||||
})
|
||||
|
||||
readonly property var reasonsSupplies: ({
|
||||
"toner-low": I18n.tr("Toner Low"),
|
||||
"toner-empty": I18n.tr("Toner Empty"),
|
||||
"marker-supply-low": I18n.tr("Marker Supply Low"),
|
||||
"marker-supply-empty": I18n.tr("Marker Supply Empty"),
|
||||
"marker-waste-almost-full": I18n.tr("Marker Waste Almost Full"),
|
||||
"marker-waste-full": I18n.tr("Marker Waste Full")
|
||||
})
|
||||
|
||||
readonly property var reasonsMedia: ({
|
||||
"media-low": I18n.tr("Media Low"),
|
||||
"media-empty": I18n.tr("Media Empty"),
|
||||
"media-needed": I18n.tr("Media Needed"),
|
||||
"media-jam": I18n.tr("Media Jam")
|
||||
})
|
||||
|
||||
readonly property var reasonsParts: ({
|
||||
"cover-open": I18n.tr("Cover Open"),
|
||||
"door-open": I18n.tr("Door Open"),
|
||||
"interlock-open": I18n.tr("Interlock Open"),
|
||||
"output-tray-missing": I18n.tr("Output Tray Missing"),
|
||||
"output-area-almost-full": I18n.tr("Output Area Almost Full"),
|
||||
"output-area-full": I18n.tr("Output Area Full")
|
||||
})
|
||||
|
||||
readonly property var reasonsErrors: ({
|
||||
"paused": I18n.tr("Paused"),
|
||||
"shutdown": I18n.tr("Shutdown"),
|
||||
"connecting-to-device": I18n.tr("Connecting to Device"),
|
||||
"timed-out": I18n.tr("Timed Out"),
|
||||
"stopping": I18n.tr("Stopping"),
|
||||
"stopped-partly": I18n.tr("Stopped Partly")
|
||||
})
|
||||
|
||||
readonly property var reasonsService: ({
|
||||
"spool-area-full": I18n.tr("Spool Area Full"),
|
||||
"cups-missing-filter-warning": I18n.tr("CUPS Missing Filter Warning"),
|
||||
"cups-insecure-filter-warning": I18n.tr("CUPS Insecure Filter Warning")
|
||||
})
|
||||
|
||||
readonly property var reasonsConnectivity: ({
|
||||
"offline-report": I18n.tr("Offline Report"),
|
||||
"moving-to-paused": I18n.tr("Moving to Paused")
|
||||
})
|
||||
|
||||
readonly property var severitySuffixes: ({
|
||||
"-error": I18n.tr("Error"),
|
||||
"-warning": I18n.tr("Warning"),
|
||||
"-report": I18n.tr("Report")
|
||||
})
|
||||
|
||||
function getPrinterStateTranslation(state) {
|
||||
return states[state] || state
|
||||
}
|
||||
|
||||
function getPrinterStateReasonTranslation(reason) {
|
||||
let allReasons = Object.assign({}, reasonsGeneral, reasonsSupplies, reasonsMedia, reasonsParts, reasonsErrors, reasonsService, reasonsConnectivity)
|
||||
|
||||
let basReason = reason
|
||||
let suffix = ""
|
||||
|
||||
for (let s in severitySuffixes) {
|
||||
if (reason.endsWith(s)) {
|
||||
basReason = reason.slice(0, -s.length)
|
||||
suffix = severitySuffixes[s]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let translation = allReasons[basReason] || basReason
|
||||
return suffix ? translation + " (" + suffix + ")" : translation
|
||||
}
|
||||
}
|
||||
@@ -1,936 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool brightnessAvailable: devices.length > 0
|
||||
property var devices: []
|
||||
property var deviceBrightness: ({})
|
||||
property var deviceBrightnessUserSet: ({})
|
||||
property var deviceMaxCache: ({})
|
||||
property int brightnessVersion: 0
|
||||
property string currentDevice: ""
|
||||
property string lastIpcDevice: ""
|
||||
property int brightnessLevel: {
|
||||
brightnessVersion
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) {
|
||||
return 50
|
||||
}
|
||||
|
||||
return getDeviceBrightness(deviceToUse)
|
||||
}
|
||||
property int maxBrightness: 100
|
||||
property bool brightnessInitialized: false
|
||||
|
||||
signal brightnessChanged(bool showOsd)
|
||||
signal deviceSwitched
|
||||
|
||||
property bool nightModeActive: nightModeEnabled
|
||||
|
||||
property bool nightModeEnabled: false
|
||||
property bool automationAvailable: false
|
||||
property bool gammaControlAvailable: false
|
||||
|
||||
function updateSingleDevice(device) {
|
||||
const deviceIndex = devices.findIndex(d => d.id === device.id)
|
||||
if (deviceIndex !== -1) {
|
||||
const newDevices = [...devices]
|
||||
const existingDevice = devices[deviceIndex]
|
||||
const cachedMax = deviceMaxCache[device.id]
|
||||
|
||||
let displayMax = cachedMax || (device.class === "ddc" ? device.max : 100)
|
||||
if (displayMax > 0 && !cachedMax) {
|
||||
const newCache = Object.assign({}, deviceMaxCache)
|
||||
newCache[device.id] = displayMax
|
||||
deviceMaxCache = newCache
|
||||
}
|
||||
|
||||
newDevices[deviceIndex] = {
|
||||
"id": device.id,
|
||||
"name": device.id,
|
||||
"class": device.class,
|
||||
"current": device.current,
|
||||
"percentage": device.currentPercent,
|
||||
"max": device.max,
|
||||
"backend": device.backend,
|
||||
"displayMax": displayMax
|
||||
}
|
||||
devices = newDevices
|
||||
}
|
||||
|
||||
const isExponential = SessionData.getBrightnessExponential(device.id)
|
||||
const userSetValue = deviceBrightnessUserSet[device.id]
|
||||
|
||||
let displayValue = device.currentPercent
|
||||
if (isExponential) {
|
||||
if (userSetValue !== undefined) {
|
||||
displayValue = userSetValue
|
||||
} else {
|
||||
displayValue = linearToExponential(device.currentPercent, device.id)
|
||||
}
|
||||
}
|
||||
|
||||
const newBrightness = Object.assign({}, deviceBrightness)
|
||||
newBrightness[device.id] = displayValue
|
||||
deviceBrightness = newBrightness
|
||||
brightnessVersion++
|
||||
}
|
||||
|
||||
function updateFromBrightnessState(state) {
|
||||
if (!state || !state.devices) {
|
||||
return
|
||||
}
|
||||
|
||||
const newMaxCache = Object.assign({}, deviceMaxCache)
|
||||
devices = state.devices.map(d => {
|
||||
const cachedMax = deviceMaxCache[d.id]
|
||||
let displayMax = cachedMax || (d.class === "ddc" ? d.max : 100)
|
||||
if (displayMax > 0 && !cachedMax) {
|
||||
newMaxCache[d.id] = displayMax
|
||||
}
|
||||
return {
|
||||
"id": d.id,
|
||||
"name": d.id,
|
||||
"class": d.class,
|
||||
"current": d.current,
|
||||
"percentage": d.currentPercent,
|
||||
"max": d.max,
|
||||
"backend": d.backend,
|
||||
"displayMax": displayMax
|
||||
}
|
||||
})
|
||||
deviceMaxCache = newMaxCache
|
||||
|
||||
const newBrightness = {}
|
||||
for (const device of state.devices) {
|
||||
const isExponential = SessionData.getBrightnessExponential(device.id)
|
||||
const userSetValue = deviceBrightnessUserSet[device.id]
|
||||
|
||||
if (isExponential) {
|
||||
if (userSetValue !== undefined) {
|
||||
newBrightness[device.id] = userSetValue
|
||||
} else {
|
||||
newBrightness[device.id] = linearToExponential(device.currentPercent, device.id)
|
||||
}
|
||||
} else {
|
||||
newBrightness[device.id] = device.currentPercent
|
||||
}
|
||||
}
|
||||
deviceBrightness = newBrightness
|
||||
brightnessVersion++
|
||||
|
||||
brightnessAvailable = devices.length > 0
|
||||
|
||||
if (devices.length > 0 && !currentDevice) {
|
||||
const lastDevice = SessionData.lastBrightnessDevice || ""
|
||||
const deviceExists = devices.some(d => d.id === lastDevice)
|
||||
if (deviceExists) {
|
||||
setCurrentDevice(lastDevice, false)
|
||||
} else {
|
||||
const backlight = devices.find(d => d.class === "backlight")
|
||||
const nonKbdDevice = devices.find(d => !d.id.includes("kbd"))
|
||||
const defaultDevice = backlight || nonKbdDevice || devices[0]
|
||||
setCurrentDevice(defaultDevice.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!brightnessInitialized) {
|
||||
brightnessInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(percentage, device, suppressOsd) {
|
||||
const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
|
||||
|
||||
if (!actualDevice) {
|
||||
console.warn("DisplayService: No device selected for brightness change")
|
||||
return
|
||||
}
|
||||
|
||||
const deviceInfo = getCurrentDeviceInfoByName(actualDevice)
|
||||
const isExponential = SessionData.getBrightnessExponential(actualDevice)
|
||||
|
||||
let minValue = 0
|
||||
let maxValue = 100
|
||||
|
||||
if (isExponential) {
|
||||
minValue = 1
|
||||
maxValue = 100
|
||||
} else {
|
||||
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
|
||||
maxValue = deviceInfo?.displayMax || 100
|
||||
}
|
||||
|
||||
if (maxValue <= 0) {
|
||||
console.warn("DisplayService: Invalid max value for device", actualDevice, "- skipping brightness change")
|
||||
return
|
||||
}
|
||||
|
||||
const clampedValue = Math.max(minValue, Math.min(maxValue, percentage))
|
||||
|
||||
if (!DMSService.isConnected) {
|
||||
console.warn("DisplayService: Not connected to DMS")
|
||||
return
|
||||
}
|
||||
|
||||
const newBrightness = Object.assign({}, deviceBrightness)
|
||||
newBrightness[actualDevice] = clampedValue
|
||||
deviceBrightness = newBrightness
|
||||
brightnessVersion++
|
||||
|
||||
if (isExponential) {
|
||||
const newUserSet = Object.assign({}, deviceBrightnessUserSet)
|
||||
newUserSet[actualDevice] = clampedValue
|
||||
deviceBrightnessUserSet = newUserSet
|
||||
SessionData.setBrightnessUserSetValue(actualDevice, clampedValue)
|
||||
}
|
||||
|
||||
if (!suppressOsd) {
|
||||
brightnessChanged(true)
|
||||
}
|
||||
|
||||
const params = {
|
||||
"device": actualDevice,
|
||||
"percent": clampedValue
|
||||
}
|
||||
if (isExponential) {
|
||||
params.exponential = true
|
||||
params.exponent = SessionData.getBrightnessExponent(actualDevice)
|
||||
}
|
||||
|
||||
DMSService.sendRequest("brightness.setBrightness", params, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set brightness:", response.error)
|
||||
ToastService.showError("Failed to set brightness: " + response.error, "", "", "brightness")
|
||||
} else {
|
||||
ToastService.dismissCategory("brightness")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setCurrentDevice(deviceName, saveToSession = false) {
|
||||
if (currentDevice === deviceName) {
|
||||
return
|
||||
}
|
||||
|
||||
currentDevice = deviceName
|
||||
lastIpcDevice = deviceName
|
||||
|
||||
if (saveToSession) {
|
||||
SessionData.setLastBrightnessDevice(deviceName)
|
||||
}
|
||||
|
||||
deviceSwitched()
|
||||
}
|
||||
|
||||
function getDeviceBrightness(deviceName) {
|
||||
if (!deviceName) {
|
||||
return 50
|
||||
}
|
||||
|
||||
if (deviceName in deviceBrightness) {
|
||||
return deviceBrightness[deviceName]
|
||||
}
|
||||
|
||||
return 50
|
||||
}
|
||||
|
||||
function linearToExponential(linearPercent, deviceName) {
|
||||
const exponent = SessionData.getBrightnessExponent(deviceName)
|
||||
const hardwarePercent = linearPercent / 100.0
|
||||
const normalizedPercent = Math.pow(hardwarePercent, 1.0 / exponent)
|
||||
return Math.round(normalizedPercent * 100.0)
|
||||
}
|
||||
|
||||
function getDefaultDevice() {
|
||||
for (const device of devices) {
|
||||
if (device.class === "backlight") {
|
||||
return device.id
|
||||
}
|
||||
}
|
||||
return devices.length > 0 ? devices[0].id : ""
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfo() {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.id === deviceToUse) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isCurrentDeviceReady() {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
return deviceToUse !== ""
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfoByName(deviceName) {
|
||||
if (!deviceName) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.id === deviceName) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getDeviceMax(deviceName) {
|
||||
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
|
||||
if (!deviceInfo) {
|
||||
return 100
|
||||
}
|
||||
return deviceInfo.displayMax || 100
|
||||
}
|
||||
|
||||
// Night Mode Functions - Simplified
|
||||
function enableNightMode() {
|
||||
if (!gammaControlAvailable) {
|
||||
ToastService.showWarning("Night mode failed: DMS gamma control not available")
|
||||
return
|
||||
}
|
||||
|
||||
nightModeEnabled = true
|
||||
SessionData.setNightModeEnabled(true)
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setEnabled", {
|
||||
"enabled": true
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to enable gamma control:", response.error)
|
||||
ToastService.showError("Failed to enable night mode: " + response.error, "", "", "night-mode")
|
||||
nightModeEnabled = false
|
||||
SessionData.setNightModeEnabled(false)
|
||||
return
|
||||
}
|
||||
ToastService.dismissCategory("night-mode")
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
} else {
|
||||
applyNightModeDirectly()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function disableNightMode() {
|
||||
nightModeEnabled = false
|
||||
SessionData.setNightModeEnabled(false)
|
||||
|
||||
if (!gammaControlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setEnabled", {
|
||||
"enabled": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable gamma control:", response.error)
|
||||
ToastService.showError("Failed to disable night mode: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function toggleNightMode() {
|
||||
if (nightModeEnabled) {
|
||||
disableNightMode()
|
||||
} else {
|
||||
enableNightMode()
|
||||
}
|
||||
}
|
||||
|
||||
function applyNightModeDirectly() {
|
||||
const temperature = SessionData.nightModeTemperature || 4000
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setManualTimes", {
|
||||
"sunrise": null,
|
||||
"sunset": null
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to clear manual times:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable IP location:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||
"low": temperature,
|
||||
"high": 6500
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set temperature:", response.error)
|
||||
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startAutomation() {
|
||||
if (!automationAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
const mode = SessionData.nightModeAutoMode || "time"
|
||||
|
||||
switch (mode) {
|
||||
case "time":
|
||||
startTimeBasedMode()
|
||||
break
|
||||
case "location":
|
||||
startLocationBasedMode()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function startTimeBasedMode() {
|
||||
const temperature = SessionData.nightModeTemperature || 4000
|
||||
const highTemp = SessionData.nightModeHighTemperature || 6500
|
||||
const sunriseHour = SessionData.nightModeEndHour
|
||||
const sunriseMinute = SessionData.nightModeEndMinute
|
||||
const sunsetHour = SessionData.nightModeStartHour
|
||||
const sunsetMinute = SessionData.nightModeStartMinute
|
||||
|
||||
const sunrise = `${String(sunriseHour).padStart(2, '0')}:${String(sunriseMinute).padStart(2, '0')}`
|
||||
const sunset = `${String(sunsetHour).padStart(2, '0')}:${String(sunsetMinute).padStart(2, '0')}`
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable IP location:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||
"low": temperature,
|
||||
"high": highTemp
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set temperature:", response.error)
|
||||
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setManualTimes", {
|
||||
"sunrise": sunrise,
|
||||
"sunset": sunset
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set manual times:", response.error)
|
||||
ToastService.showError("Failed to set night mode schedule: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startLocationBasedMode() {
|
||||
const temperature = SessionData.nightModeTemperature || 4000
|
||||
const highTemp = SessionData.nightModeHighTemperature || 6500
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setManualTimes", {
|
||||
"sunrise": null,
|
||||
"sunset": null
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to clear manual times:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||
"low": temperature,
|
||||
"high": highTemp
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set temperature:", response.error)
|
||||
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
|
||||
return
|
||||
}
|
||||
|
||||
if (SessionData.nightModeUseIPLocation) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": true
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to enable IP location:", response.error)
|
||||
ToastService.showError("Failed to enable IP location: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||
"use": false
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to disable IP location:", response.error)
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||
"latitude": SessionData.latitude,
|
||||
"longitude": SessionData.longitude
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to set location:", response.error)
|
||||
ToastService.showError("Failed to set night mode location: " + response.error, "", "", "night-mode")
|
||||
} else {
|
||||
ToastService.dismissCategory("night-mode")
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.warn("DisplayService: Location mode selected but no coordinates set and IP location disabled")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setNightModeAutomationMode(mode) {
|
||||
SessionData.setNightModeAutoMode(mode)
|
||||
}
|
||||
|
||||
function evaluateNightMode() {
|
||||
if (!nightModeEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
restartTimer.nextAction = "automation"
|
||||
restartTimer.start()
|
||||
} else {
|
||||
restartTimer.nextAction = "direct"
|
||||
restartTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
function checkGammaControlAvailability() {
|
||||
if (!DMSService.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.apiVersion < 6) {
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!DMSService.capabilities.includes("gamma")) {
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wayland.gamma.getState", null, response => {
|
||||
if (response.error) {
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
console.error("DisplayService: Gamma control not available:", response.error)
|
||||
} else {
|
||||
gammaControlAvailable = true
|
||||
automationAvailable = true
|
||||
|
||||
if (nightModeEnabled) {
|
||||
DMSService.sendRequest("wayland.gamma.setEnabled", {
|
||||
"enabled": true
|
||||
}, enableResponse => {
|
||||
if (enableResponse.error) {
|
||||
console.error("DisplayService: Failed to enable gamma control on startup:", enableResponse.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
} else {
|
||||
applyNightModeDirectly()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: restartTimer
|
||||
property string nextAction: ""
|
||||
interval: 100
|
||||
repeat: false
|
||||
|
||||
onTriggered: {
|
||||
if (nextAction === "automation") {
|
||||
startAutomation()
|
||||
} else if (nextAction === "direct") {
|
||||
applyNightModeDirectly()
|
||||
}
|
||||
nextAction = ""
|
||||
}
|
||||
}
|
||||
|
||||
function rescanDevices() {
|
||||
if (!DMSService.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("brightness.rescan", null, response => {
|
||||
if (response.error) {
|
||||
console.error("DisplayService: Failed to rescan brightness devices:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateDeviceBrightnessDisplay(deviceName) {
|
||||
brightnessVersion++
|
||||
brightnessChanged()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
nightModeEnabled = SessionData.nightModeEnabled
|
||||
deviceBrightnessUserSet = Object.assign({}, SessionData.brightnessUserSetValues)
|
||||
if (DMSService.isConnected) {
|
||||
checkGammaControlAvailability()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
|
||||
function onScreensChanged() {
|
||||
rescanDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkGammaControlAvailability()
|
||||
} else {
|
||||
brightnessAvailable = false
|
||||
gammaControlAvailable = false
|
||||
automationAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCapabilitiesReceived() {
|
||||
checkGammaControlAvailability()
|
||||
}
|
||||
|
||||
function onBrightnessStateUpdate(data) {
|
||||
updateFromBrightnessState(data)
|
||||
}
|
||||
|
||||
function onBrightnessDeviceUpdate(device) {
|
||||
updateSingleDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
// Session Data Connections
|
||||
Connections {
|
||||
target: SessionData
|
||||
|
||||
function onNightModeEnabledChanged() {
|
||||
nightModeEnabled = SessionData.nightModeEnabled
|
||||
evaluateNightMode()
|
||||
}
|
||||
|
||||
function onNightModeAutoEnabledChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeAutoModeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeStartHourChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeStartMinuteChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeEndHourChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeEndMinuteChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeTemperatureChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeHighTemperatureChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onLatitudeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onLongitudeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeUseIPLocationChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
function set(percentage: string, device: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const value = parseInt(percentage)
|
||||
if (isNaN(value)) {
|
||||
return "Invalid brightness value: " + percentage
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
|
||||
if (targetDevice && !root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
const deviceInfo = targetDevice ? root.getCurrentDeviceInfoByName(targetDevice) : null
|
||||
const minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
|
||||
const clampedValue = Math.max(minValue, Math.min(100, value))
|
||||
|
||||
root.lastIpcDevice = targetDevice
|
||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(targetDevice, false)
|
||||
}
|
||||
root.setBrightness(clampedValue, targetDevice, false)
|
||||
|
||||
if (targetDevice) {
|
||||
return "Brightness set to " + clampedValue + "% on " + targetDevice
|
||||
} else {
|
||||
return "Brightness set to " + clampedValue + "%"
|
||||
}
|
||||
}
|
||||
|
||||
function increment(step: string, device: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
|
||||
return "Device not found: " + actualDevice
|
||||
}
|
||||
|
||||
const stepValue = parseInt(step || "5")
|
||||
|
||||
root.lastIpcDevice = actualDevice
|
||||
if (actualDevice && actualDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(actualDevice, false)
|
||||
}
|
||||
|
||||
const isExponential = SessionData.getBrightnessExponential(actualDevice)
|
||||
const currentBrightness = root.getDeviceBrightness(actualDevice)
|
||||
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice)
|
||||
|
||||
let maxValue = 100
|
||||
if (isExponential) {
|
||||
maxValue = 100
|
||||
} else {
|
||||
maxValue = deviceInfo?.displayMax || 100
|
||||
}
|
||||
|
||||
const newBrightness = Math.min(maxValue, currentBrightness + stepValue)
|
||||
|
||||
root.setBrightness(newBrightness, actualDevice, false)
|
||||
|
||||
return "Brightness increased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "")
|
||||
}
|
||||
|
||||
function decrement(step: string, device: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
||||
|
||||
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
|
||||
return "Device not found: " + actualDevice
|
||||
}
|
||||
|
||||
const stepValue = parseInt(step || "5")
|
||||
|
||||
root.lastIpcDevice = actualDevice
|
||||
if (actualDevice && actualDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(actualDevice, false)
|
||||
}
|
||||
|
||||
const isExponential = SessionData.getBrightnessExponential(actualDevice)
|
||||
const currentBrightness = root.getDeviceBrightness(actualDevice)
|
||||
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice)
|
||||
|
||||
let minValue = 0
|
||||
if (isExponential) {
|
||||
minValue = 1
|
||||
} else {
|
||||
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
|
||||
}
|
||||
|
||||
const newBrightness = Math.max(minValue, currentBrightness - stepValue)
|
||||
|
||||
root.setBrightness(newBrightness, actualDevice, false)
|
||||
|
||||
return "Brightness decreased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "")
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "No brightness devices available"
|
||||
}
|
||||
|
||||
let result = "Available devices:\n"
|
||||
for (const device of root.devices) {
|
||||
const isExp = SessionData.getBrightnessExponential(device.id)
|
||||
result += device.id + " (" + device.class + ")" + (isExp ? " [exponential]" : "") + "\n"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function enableExponential(device: string): string {
|
||||
const targetDevice = device || root.currentDevice
|
||||
if (!targetDevice) {
|
||||
return "No device specified"
|
||||
}
|
||||
|
||||
if (!root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
SessionData.setBrightnessExponential(targetDevice, true)
|
||||
return "Exponential mode enabled for " + targetDevice
|
||||
}
|
||||
|
||||
function disableExponential(device: string): string {
|
||||
const targetDevice = device || root.currentDevice
|
||||
if (!targetDevice) {
|
||||
return "No device specified"
|
||||
}
|
||||
|
||||
if (!root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
SessionData.setBrightnessExponential(targetDevice, false)
|
||||
return "Exponential mode disabled for " + targetDevice
|
||||
}
|
||||
|
||||
function toggleExponential(device: string): string {
|
||||
const targetDevice = device || root.currentDevice
|
||||
if (!targetDevice) {
|
||||
return "No device specified"
|
||||
}
|
||||
|
||||
if (!root.devices.some(d => d.id === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
const currentState = SessionData.getBrightnessExponential(targetDevice)
|
||||
SessionData.setBrightnessExponential(targetDevice, !currentState)
|
||||
return "Exponential mode " + (!currentState ? "enabled" : "disabled") + " for " + targetDevice
|
||||
}
|
||||
|
||||
target: "brightness"
|
||||
}
|
||||
|
||||
// IPC Handler for night mode control
|
||||
IpcHandler {
|
||||
function toggle(): string {
|
||||
root.toggleNightMode()
|
||||
return root.nightModeEnabled ? "Night mode enabled" : "Night mode disabled"
|
||||
}
|
||||
|
||||
function enable(): string {
|
||||
root.enableNightMode()
|
||||
return "Night mode enabled"
|
||||
}
|
||||
|
||||
function disable(): string {
|
||||
root.disableNightMode()
|
||||
return "Night mode disabled"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled"
|
||||
}
|
||||
|
||||
function temperature(value: string): string {
|
||||
if (!value) {
|
||||
return "Current temperature: " + SessionData.nightModeTemperature + "K"
|
||||
}
|
||||
|
||||
const temp = parseInt(value)
|
||||
if (isNaN(temp)) {
|
||||
return "Invalid temperature. Use a value between 2500 and 6000 (in steps of 500)"
|
||||
}
|
||||
|
||||
// Validate temperature is in valid range and steps
|
||||
if (temp < 2500 || temp > 6000) {
|
||||
return "Temperature must be between 2500K and 6000K"
|
||||
}
|
||||
|
||||
// Round to nearest 500
|
||||
const rounded = Math.round(temp / 500) * 500
|
||||
|
||||
SessionData.setNightModeTemperature(rounded)
|
||||
|
||||
// Restart night mode with new temperature if active
|
||||
if (root.nightModeEnabled) {
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
root.startAutomation()
|
||||
} else {
|
||||
root.applyNightModeDirectly()
|
||||
}
|
||||
}
|
||||
|
||||
if (rounded !== temp) {
|
||||
return "Night mode temperature set to " + rounded + "K (rounded from " + temp + "K)"
|
||||
} else {
|
||||
return "Night mode temperature set to " + rounded + "K"
|
||||
}
|
||||
}
|
||||
|
||||
target: "night"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string username: ""
|
||||
property string fullName: ""
|
||||
property string profilePicture: ""
|
||||
property string uptime: ""
|
||||
property string shortUptime: ""
|
||||
property string hostname: ""
|
||||
property bool profileAvailable: false
|
||||
|
||||
function getUserInfo() {
|
||||
Proc.runCommand("userInfo", ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.username = "User"
|
||||
root.fullName = "User"
|
||||
root.hostname = "System"
|
||||
return
|
||||
}
|
||||
const parts = output.trim().split("|")
|
||||
if (parts.length >= 3) {
|
||||
root.username = parts[0] || ""
|
||||
root.fullName = parts[1] || parts[0] || ""
|
||||
root.hostname = parts[2] || ""
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function getUptime() {
|
||||
Proc.runCommand("uptime", ["cat", "/proc/uptime"], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.uptime = "Unknown"
|
||||
return
|
||||
}
|
||||
const seconds = parseInt(output.split(" ")[0])
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
const parts = []
|
||||
if (days > 0) {
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
root.uptime = `up ${parts.join(", ")}`
|
||||
} else {
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
}
|
||||
|
||||
let shortUptime = "up"
|
||||
if (days > 0) {
|
||||
shortUptime += ` ${days}d`
|
||||
}
|
||||
if (hours > 0) {
|
||||
shortUptime += ` ${hours}h`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
shortUptime += ` ${minutes}m`
|
||||
}
|
||||
root.shortUptime = shortUptime
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function refreshUserInfo() {
|
||||
getUserInfo()
|
||||
getUptime()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
getUserInfo()
|
||||
getUptime()
|
||||
}
|
||||
}
|
||||
@@ -1,651 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
|
||||
property var weather: ({
|
||||
"available": false,
|
||||
"loading": true,
|
||||
"temp": 0,
|
||||
"tempF": 0,
|
||||
"feelsLike": 0,
|
||||
"feelsLikeF": 0,
|
||||
"city": "",
|
||||
"country": "",
|
||||
"wCode": 0,
|
||||
"humidity": 0,
|
||||
"wind": "",
|
||||
"sunrise": "06:00",
|
||||
"sunset": "18:00",
|
||||
"uv": 0,
|
||||
"pressure": 0,
|
||||
"precipitationProbability": 0,
|
||||
"isDay": true,
|
||||
"forecast": []
|
||||
})
|
||||
|
||||
property var location: null
|
||||
property int updateInterval: 900000 // 15 minutes
|
||||
property int retryAttempts: 0
|
||||
property int maxRetryAttempts: 3
|
||||
property int retryDelay: 30000
|
||||
property int lastFetchTime: 0
|
||||
property int minFetchInterval: 30000
|
||||
property int persistentRetryCount: 0
|
||||
|
||||
readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"]
|
||||
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
|
||||
|
||||
property var weatherIcons: ({
|
||||
"0": "clear_day",
|
||||
"1": "clear_day",
|
||||
"2": "partly_cloudy_day",
|
||||
"3": "cloud",
|
||||
"45": "foggy",
|
||||
"48": "foggy",
|
||||
"51": "rainy",
|
||||
"53": "rainy",
|
||||
"55": "rainy",
|
||||
"56": "rainy",
|
||||
"57": "rainy",
|
||||
"61": "rainy",
|
||||
"63": "rainy",
|
||||
"65": "rainy",
|
||||
"66": "rainy",
|
||||
"67": "rainy",
|
||||
"71": "cloudy_snowing",
|
||||
"73": "cloudy_snowing",
|
||||
"75": "snowing_heavy",
|
||||
"77": "cloudy_snowing",
|
||||
"80": "rainy",
|
||||
"81": "rainy",
|
||||
"82": "rainy",
|
||||
"85": "cloudy_snowing",
|
||||
"86": "snowing_heavy",
|
||||
"95": "thunderstorm",
|
||||
"96": "thunderstorm",
|
||||
"99": "thunderstorm"
|
||||
})
|
||||
|
||||
property var nightWeatherIcons: ({
|
||||
"0": "clear_night",
|
||||
"1": "clear_night",
|
||||
"2": "partly_cloudy_night",
|
||||
"3": "cloud",
|
||||
"45": "foggy",
|
||||
"48": "foggy",
|
||||
"51": "rainy",
|
||||
"53": "rainy",
|
||||
"55": "rainy",
|
||||
"56": "rainy",
|
||||
"57": "rainy",
|
||||
"61": "rainy",
|
||||
"63": "rainy",
|
||||
"65": "rainy",
|
||||
"66": "rainy",
|
||||
"67": "rainy",
|
||||
"71": "cloudy_snowing",
|
||||
"73": "cloudy_snowing",
|
||||
"75": "snowing_heavy",
|
||||
"77": "cloudy_snowing",
|
||||
"80": "rainy",
|
||||
"81": "rainy",
|
||||
"82": "rainy",
|
||||
"85": "cloudy_snowing",
|
||||
"86": "snowing_heavy",
|
||||
"95": "thunderstorm",
|
||||
"96": "thunderstorm",
|
||||
"99": "thunderstorm"
|
||||
})
|
||||
|
||||
function getWeatherIcon(code, isDay) {
|
||||
if (typeof isDay === "undefined") {
|
||||
isDay = weather.isDay
|
||||
}
|
||||
const iconMap = isDay ? weatherIcons : nightWeatherIcons
|
||||
return iconMap[String(code)] || "cloud"
|
||||
}
|
||||
|
||||
function getWeatherCondition(code) {
|
||||
const conditions = {
|
||||
"0": "Clear",
|
||||
"1": "Clear",
|
||||
"2": "Partly cloudy",
|
||||
"3": "Overcast",
|
||||
"45": "Fog",
|
||||
"48": "Fog",
|
||||
"51": "Drizzle",
|
||||
"53": "Drizzle",
|
||||
"55": "Drizzle",
|
||||
"56": "Freezing drizzle",
|
||||
"57": "Freezing drizzle",
|
||||
"61": "Light rain",
|
||||
"63": "Rain",
|
||||
"65": "Heavy rain",
|
||||
"66": "Light rain",
|
||||
"67": "Heavy rain",
|
||||
"71": "Light snow",
|
||||
"73": "Snow",
|
||||
"75": "Heavy snow",
|
||||
"77": "Snow",
|
||||
"80": "Light rain",
|
||||
"81": "Rain",
|
||||
"82": "Heavy rain",
|
||||
"85": "Light snow showers",
|
||||
"86": "Heavy snow showers",
|
||||
"95": "Thunderstorm",
|
||||
"96": "Thunderstorm with hail",
|
||||
"99": "Thunderstorm with hail"
|
||||
}
|
||||
return conditions[String(code)] || "Unknown"
|
||||
}
|
||||
|
||||
function formatTime(isoString) {
|
||||
if (!isoString) return "--"
|
||||
|
||||
try {
|
||||
const date = new Date(isoString)
|
||||
const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
|
||||
return date.toLocaleTimeString(Qt.locale(), format)
|
||||
} catch (e) {
|
||||
return "--"
|
||||
}
|
||||
}
|
||||
|
||||
function formatForecastDay(isoString, index) {
|
||||
if (!isoString) return "--"
|
||||
|
||||
try {
|
||||
const date = new Date(isoString)
|
||||
if (index === 0) return I18n.tr("Today")
|
||||
if (index === 1) return I18n.tr("Tomorrow")
|
||||
|
||||
const locale = Qt.locale()
|
||||
return locale.dayName(date.getDay(), Locale.ShortFormat)
|
||||
} catch (e) {
|
||||
return "--"
|
||||
}
|
||||
}
|
||||
|
||||
function getWeatherApiUrl() {
|
||||
if (!location) {
|
||||
return null
|
||||
}
|
||||
|
||||
const params = [
|
||||
"latitude=" + location.latitude,
|
||||
"longitude=" + location.longitude,
|
||||
"current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m",
|
||||
"daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max",
|
||||
"timezone=auto",
|
||||
"forecast_days=7"
|
||||
]
|
||||
|
||||
if (SettingsData.useFahrenheit) {
|
||||
params.push("temperature_unit=fahrenheit")
|
||||
}
|
||||
|
||||
return "https://api.open-meteo.com/v1/forecast?" + params.join('&')
|
||||
}
|
||||
|
||||
function getGeocodingUrl(query) {
|
||||
return "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(query) + "&count=1&language=en&format=json"
|
||||
}
|
||||
|
||||
function addRef() {
|
||||
refCount++
|
||||
|
||||
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
|
||||
fetchWeather()
|
||||
}
|
||||
}
|
||||
|
||||
function removeRef() {
|
||||
refCount = Math.max(0, refCount - 1)
|
||||
}
|
||||
|
||||
function updateLocation() {
|
||||
if (SettingsData.useAutoLocation) {
|
||||
getLocationFromIP()
|
||||
} else {
|
||||
const coords = SettingsData.weatherCoordinates
|
||||
if (coords) {
|
||||
const parts = coords.split(",")
|
||||
if (parts.length === 2) {
|
||||
const lat = parseFloat(parts[0])
|
||||
const lon = parseFloat(parts[1])
|
||||
if (!isNaN(lat) && !isNaN(lon)) {
|
||||
getLocationFromCoords(lat, lon)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cityName = SettingsData.weatherLocation
|
||||
if (cityName) {
|
||||
getLocationFromCity(cityName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocationFromCoords(lat, lon) {
|
||||
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en"
|
||||
reverseGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url])
|
||||
reverseGeocodeFetcher.running = true
|
||||
}
|
||||
|
||||
function getLocationFromCity(city) {
|
||||
cityGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([getGeocodingUrl(city)])
|
||||
cityGeocodeFetcher.running = true
|
||||
}
|
||||
|
||||
function getLocationFromIP() {
|
||||
ipLocationFetcher.running = true
|
||||
}
|
||||
|
||||
function fetchWeather() {
|
||||
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!location) {
|
||||
updateLocation()
|
||||
return
|
||||
}
|
||||
|
||||
if (weatherFetcher.running) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
if (now - root.lastFetchTime < root.minFetchInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
const apiUrl = getWeatherApiUrl()
|
||||
if (!apiUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
root.lastFetchTime = now
|
||||
root.weather.loading = true
|
||||
const weatherCmd = lowPriorityCmd.concat(["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "150k", "--compressed"])
|
||||
weatherFetcher.command = weatherCmd.concat([apiUrl])
|
||||
weatherFetcher.running = true
|
||||
}
|
||||
|
||||
function forceRefresh() {
|
||||
root.lastFetchTime = 0 // Reset throttle
|
||||
fetchWeather()
|
||||
}
|
||||
|
||||
function nextInterval() {
|
||||
const jitter = Math.floor(Math.random() * 15000) - 7500
|
||||
return Math.max(60000, root.updateInterval + jitter)
|
||||
}
|
||||
|
||||
function handleWeatherSuccess() {
|
||||
root.retryAttempts = 0
|
||||
root.persistentRetryCount = 0
|
||||
if (persistentRetryTimer.running) {
|
||||
persistentRetryTimer.stop()
|
||||
}
|
||||
if (updateTimer.interval !== root.updateInterval) {
|
||||
updateTimer.interval = root.updateInterval
|
||||
}
|
||||
}
|
||||
|
||||
function handleWeatherFailure() {
|
||||
root.retryAttempts++
|
||||
if (root.retryAttempts < root.maxRetryAttempts) {
|
||||
retryTimer.start()
|
||||
} else {
|
||||
root.retryAttempts = 0
|
||||
if (!root.weather.available) {
|
||||
root.weather.loading = false
|
||||
}
|
||||
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
|
||||
persistentRetryCount++
|
||||
persistentRetryTimer.interval = backoffDelay
|
||||
persistentRetryTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ipLocationFetcher
|
||||
command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ip-api.com/json/"])
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
root.handleWeatherFailure()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
if (data.status === "fail") {
|
||||
throw new Error("IP location lookup failed")
|
||||
}
|
||||
|
||||
const lat = parseFloat(data.lat)
|
||||
const lon = parseFloat(data.lon)
|
||||
const city = data.city
|
||||
|
||||
if (!city || isNaN(lat) || isNaN(lon)) {
|
||||
throw new Error("Missing or invalid location data")
|
||||
}
|
||||
|
||||
root.location = {
|
||||
city: city,
|
||||
latitude: lat,
|
||||
longitude: lon
|
||||
}
|
||||
fetchWeather()
|
||||
} catch (e) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: reverseGeocodeFetcher
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
root.handleWeatherFailure()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
const address = data.address || {}
|
||||
|
||||
root.location = {
|
||||
city: address.hamlet || address.city || address.town || address.village || "Unknown",
|
||||
country: address.country || "Unknown",
|
||||
latitude: parseFloat(data.lat),
|
||||
longitude: parseFloat(data.lon)
|
||||
}
|
||||
|
||||
fetchWeather()
|
||||
} catch (e) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cityGeocodeFetcher
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
root.handleWeatherFailure()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
const results = data.results
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
throw new Error("No results found")
|
||||
}
|
||||
|
||||
const result = results[0]
|
||||
|
||||
root.location = {
|
||||
city: result.name,
|
||||
country: result.country,
|
||||
latitude: result.latitude,
|
||||
longitude: result.longitude
|
||||
}
|
||||
|
||||
fetchWeather()
|
||||
} catch (e) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: weatherFetcher
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
root.handleWeatherFailure()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
if (!data.current || !data.daily) {
|
||||
throw new Error("Required weather data fields missing")
|
||||
}
|
||||
|
||||
const current = data.current
|
||||
const daily = data.daily
|
||||
const currentUnits = data.current_units || {}
|
||||
|
||||
const tempC = current.temperature_2m || 0
|
||||
const tempF = SettingsData.useFahrenheit ? tempC : (tempC * 9/5 + 32)
|
||||
const feelsLikeC = current.apparent_temperature || tempC
|
||||
const feelsLikeF = SettingsData.useFahrenheit ? feelsLikeC : (feelsLikeC * 9/5 + 32)
|
||||
|
||||
const forecast = []
|
||||
if (daily.time && daily.time.length > 0) {
|
||||
for (let i = 0; i < Math.min(daily.time.length, 7); i++) {
|
||||
const tempMinC = daily.temperature_2m_min?.[i] || 0
|
||||
const tempMaxC = daily.temperature_2m_max?.[i] || 0
|
||||
const tempMinF = SettingsData.useFahrenheit ? tempMinC : (tempMinC * 9/5 + 32)
|
||||
const tempMaxF = SettingsData.useFahrenheit ? tempMaxC : (tempMaxC * 9/5 + 32)
|
||||
|
||||
forecast.push({
|
||||
"day": formatForecastDay(daily.time[i], i),
|
||||
"wCode": daily.weather_code?.[i] || 0,
|
||||
"tempMin": Math.round(tempMinC),
|
||||
"tempMax": Math.round(tempMaxC),
|
||||
"tempMinF": Math.round(tempMinF),
|
||||
"tempMaxF": Math.round(tempMaxF),
|
||||
"precipitationProbability": Math.round(daily.precipitation_probability_max?.[i] || 0),
|
||||
"sunrise": daily.sunrise?.[i] ? formatTime(daily.sunrise[i]) : "",
|
||||
"sunset": daily.sunset?.[i] ? formatTime(daily.sunset[i]) : ""
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
root.weather = {
|
||||
"available": true,
|
||||
"loading": false,
|
||||
"temp": Math.round(tempC),
|
||||
"tempF": Math.round(tempF),
|
||||
"feelsLike": Math.round(feelsLikeC),
|
||||
"feelsLikeF": Math.round(feelsLikeF),
|
||||
"city": root.location?.city || "Unknown",
|
||||
"country": root.location?.country || "Unknown",
|
||||
"wCode": current.weather_code || 0,
|
||||
"humidity": Math.round(current.relative_humidity_2m || 0),
|
||||
"wind": Math.round(current.wind_speed_10m || 0) + " " + (currentUnits.wind_speed_10m || 'm/s'),
|
||||
"sunrise": formatTime(daily.sunrise?.[0]) || "06:00",
|
||||
"sunset": formatTime(daily.sunset?.[0]) || "18:00",
|
||||
"uv": 0,
|
||||
"pressure": Math.round(current.surface_pressure || 0),
|
||||
"precipitationProbability": Math.round(daily.precipitation_probability_max?.[0] || 0),
|
||||
"isDay": Boolean(current.is_day),
|
||||
"forecast": forecast
|
||||
}
|
||||
|
||||
const displayTemp = SettingsData.useFahrenheit ? root.weather.tempF : root.weather.temp
|
||||
const unit = SettingsData.useFahrenheit ? "°F" : "°C"
|
||||
|
||||
root.handleWeatherSuccess()
|
||||
} catch (e) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: nextInterval()
|
||||
running: root.refCount > 0 && SettingsData.weatherEnabled
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
interval = nextInterval()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: root.retryDelay
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: persistentRetryTimer
|
||||
interval: 60000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.weather.available) {
|
||||
root.weather.loading = true
|
||||
}
|
||||
root.fetchWeather()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
SettingsData.weatherCoordinatesChanged.connect(() => {
|
||||
root.location = null
|
||||
root.weather = {
|
||||
"available": false,
|
||||
"loading": true,
|
||||
"temp": 0,
|
||||
"tempF": 0,
|
||||
"feelsLike": 0,
|
||||
"feelsLikeF": 0,
|
||||
"city": "",
|
||||
"country": "",
|
||||
"wCode": 0,
|
||||
"humidity": 0,
|
||||
"wind": "",
|
||||
"sunrise": "06:00",
|
||||
"sunset": "18:00",
|
||||
"uv": 0,
|
||||
"pressure": 0,
|
||||
"precipitationProbability": 0,
|
||||
"isDay": true,
|
||||
"forecast": []
|
||||
}
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.weatherLocationChanged.connect(() => {
|
||||
root.location = null
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.useAutoLocationChanged.connect(() => {
|
||||
root.location = null
|
||||
root.weather = {
|
||||
"available": false,
|
||||
"loading": true,
|
||||
"temp": 0,
|
||||
"tempF": 0,
|
||||
"feelsLike": 0,
|
||||
"feelsLikeF": 0,
|
||||
"city": "",
|
||||
"country": "",
|
||||
"wCode": 0,
|
||||
"humidity": 0,
|
||||
"wind": "",
|
||||
"sunrise": "06:00",
|
||||
"sunset": "18:00",
|
||||
"uv": 0,
|
||||
"pressure": 0,
|
||||
"precipitationProbability": 0,
|
||||
"isDay": true,
|
||||
"forecast": []
|
||||
}
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.useFahrenheitChanged.connect(() => {
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.weatherEnabledChanged.connect(() => {
|
||||
if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) {
|
||||
root.forceRefresh()
|
||||
} else if (!SettingsData.weatherEnabled) {
|
||||
updateTimer.stop()
|
||||
retryTimer.stop()
|
||||
persistentRetryTimer.stop()
|
||||
if (weatherFetcher.running) {
|
||||
weatherFetcher.running = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string blurNamespace: "dms:osd"
|
||||
WlrLayershell.namespace: blurNamespace
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property var modelData
|
||||
property bool shouldBeVisible: false
|
||||
property int autoHideInterval: 2000
|
||||
property bool enableMouseInteraction: false
|
||||
property real osdWidth: Theme.iconSize + Theme.spacingS * 2
|
||||
property real osdHeight: Theme.iconSize + Theme.spacingS * 2
|
||||
property int animationDuration: Theme.mediumDuration
|
||||
property var animationEasing: Theme.emphasizedEasing
|
||||
|
||||
signal osdShown
|
||||
signal osdHidden
|
||||
|
||||
function show() {
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
hideTimer.restart()
|
||||
osdShown()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
shouldBeVisible = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function resetHideTimer() {
|
||||
if (shouldBeVisible) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function updateHoverState() {
|
||||
let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered
|
||||
if (enableMouseInteraction) {
|
||||
if (isHovered) {
|
||||
hideTimer.stop()
|
||||
} else if (shouldBeVisible) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setChildHovered(hovered) {
|
||||
osdContainer.childHovered = hovered
|
||||
updateHoverState()
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||
readonly property real screenWidth: screen.width
|
||||
readonly property real screenHeight: screen.height
|
||||
readonly property real alignedWidth: Theme.px(osdWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(osdHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap((screenWidth - alignedWidth) / 2, dpr)
|
||||
readonly property real alignedY: Theme.snap(screenHeight - alignedHeight - Theme.spacingM, dpr)
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
|
||||
interval: autoHideInterval
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!enableMouseInteraction || !mouseArea.containsMouse) {
|
||||
hide()
|
||||
} else {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
visible = false
|
||||
osdHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: osdContainer
|
||||
x: alignedX
|
||||
y: alignedY
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
scale: shouldBeVisible ? 1 : 0.9
|
||||
|
||||
property bool childHovered: false
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha * osdContainer.opacity))
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
visible: osdContainer.popupSurfaceAlpha >= 0.95
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, osdContainer.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * osdContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, osdContainer.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: enableMouseInteraction
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onContainsMouseChanged: updateHoverState()
|
||||
}
|
||||
|
||||
onChildHoveredChanged: updateHoverState()
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.visible
|
||||
asynchronous: false
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: animationEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: animationEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: bgShadowLayer
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property string layerNamespace: "dms:popout"
|
||||
WlrLayershell.namespace: layerNamespace
|
||||
|
||||
property alias content: contentLoader.sourceComponent
|
||||
property alias contentLoader: contentLoader
|
||||
property real popupWidth: 400
|
||||
property real popupHeight: 300
|
||||
property real triggerX: 0
|
||||
property real triggerY: 0
|
||||
property real triggerWidth: 40
|
||||
property string triggerSection: ""
|
||||
property string positioning: "center"
|
||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property bool shouldBeVisible: false
|
||||
property int keyboardFocusMode: WlrKeyboardFocus.OnDemand
|
||||
|
||||
signal opened
|
||||
signal popoutClosed
|
||||
signal backgroundClicked
|
||||
|
||||
function open() {
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
opened()
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible)
|
||||
close()
|
||||
else
|
||||
open()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
visible = false
|
||||
popoutClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
default:
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldBeVisible ? keyboardFocusMode : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
readonly property real screenWidth: root.screen.width
|
||||
readonly property real screenHeight: root.screen.height
|
||||
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
||||
|
||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left) {
|
||||
return triggerY + SettingsData.dankBarBottomGap
|
||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
||||
return screenWidth - triggerY - SettingsData.dankBarBottomGap - popupWidth
|
||||
} else {
|
||||
const centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2)
|
||||
return Math.max(Theme.popupDistance, Math.min(screenWidth - popupWidth - Theme.popupDistance, centerX))
|
||||
}
|
||||
})(), dpr)
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
||||
const centerY = triggerX + (triggerWidth / 2) - (popupHeight / 2)
|
||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, centerY))
|
||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Bottom) {
|
||||
return Math.max(Theme.popupDistance, screenHeight - triggerY - popupHeight)
|
||||
} else {
|
||||
return Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY)
|
||||
}
|
||||
})(), dpr)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: shouldBeVisible && contentLoader.opacity > 0.1
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.x < alignedX || mouse.x > alignedX + alignedWidth ||
|
||||
mouse.y < alignedY || mouse.y > alignedY + alignedHeight) {
|
||||
backgroundClicked()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
x: alignedX
|
||||
y: alignedY
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
|
||||
readonly property bool barTop: SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
readonly property bool barBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr)
|
||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr)
|
||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha * contentWrapper.opacity))
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
visible: contentWrapper.popupSurfaceAlpha >= 0.95
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, contentWrapper.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * contentWrapper.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, contentWrapper.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentLoaderWrapper
|
||||
anchors.fill: parent
|
||||
x: Theme.snap(x, root.dpr)
|
||||
y: Theme.snap(y, root.dpr)
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.visible
|
||||
asynchronous: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
onVisibleChanged: if (visible) forceActiveFocus()
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,23 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="524.44849"
|
||||
height="524.5979"
|
||||
viewBox="0 0 524.44848 524.5979"
|
||||
width="482.90668"
|
||||
height="558.15088"
|
||||
viewBox="0 0 482.90667 558.15088"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
shape-rendering="geometricPrecision">
|
||||
shape-rendering="auto"
|
||||
style="image-rendering: auto; filter: url(#smoothing);">
|
||||
<defs
|
||||
id="defs1">
|
||||
<filter id="smoothing" x="-0.05" y="-0.05" width="1.1" height="1.1">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="0.5" />
|
||||
</filter>
|
||||
<color-profile
|
||||
name="sRGB IEC61966-2.1"
|
||||
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
|
||||
id="color-profile1" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath21">
|
||||
@@ -64,42 +72,42 @@
|
||||
</defs>
|
||||
<g
|
||||
id="layer-MC0"
|
||||
transform="translate(-475.12476,-548.5802)">
|
||||
transform="translate(-486.31024,-515.02722)">
|
||||
<path
|
||||
id="path20"
|
||||
d="M 0,0 C -1.568,1.568 -3.163,3.098 -4.787,4.61 -5.944,3.966 -7.185,3.35 -8.529,2.762 -9.658,2.277 -10.815,1.82 -11.981,1.4 c 1.885,-1.689 3.742,-3.425 5.552,-5.207 0.448,-0.429 0.886,-0.868 1.325,-1.306 2.221,-2.221 4.386,-4.498 6.476,-6.84 27.639,-30.784 44.453,-71.459 44.453,-116.089 0,-29.347 -7.259,-56.977 -20.09,-81.21 -2.192,-4.134 -4.544,-8.174 -7.054,-12.102 -6.83,-10.74 -14.827,-20.669 -23.785,-29.636 -5.944,-5.944 -12.317,-11.459 -19.073,-16.498 -0.56,-0.42 -1.12,-0.83 -1.689,-1.231 -28.675,-20.893 -63.975,-33.201 -102.186,-33.201 -48.018,0 -91.464,19.456 -122.948,50.93 -0.737,0.737 -1.465,1.474 -2.174,2.221 -0.55,0.569 -1.101,1.147 -1.633,1.726 -15.545,16.553 -27.881,36.14 -36.018,57.779 -3.098,8.211 -5.58,16.712 -7.409,25.464 -2.417,11.534 -3.686,23.496 -3.686,35.758 0.01,42.326 15.117,81.097 40.246,111.246 -2.072,1.278 -3.975,2.809 -5.534,4.637 -26.174,-31.399 -41.934,-71.822 -41.934,-115.883 0,-18.187 2.678,-35.748 7.67,-52.311 3.359,-11.142 7.754,-21.835 13.092,-31.95 8.528,-16.208 19.446,-30.961 32.276,-43.801 1.251,-1.25 2.529,-2.491 3.817,-3.685 0.662,-0.644 1.334,-1.26 2.006,-1.876 10.862,-9.938 22.945,-18.578 36,-25.661 25.632,-13.913 55.016,-21.816 86.229,-21.816 2.454,0 4.908,0.047 7.334,0.14 36.056,1.446 69.424,13.427 97.054,32.976 0.569,0.392 1.148,0.793 1.698,1.204 7.82,5.655 15.164,11.925 21.966,18.718 7.904,7.904 15.07,16.526 21.406,25.773 2.556,3.723 4.973,7.558 7.25,11.477 15.499,26.697 24.382,57.723 24.382,90.812 C 53.038,-78.055 32.762,-32.762 0,0"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.4480327,0,0,-1.2531805,922.77315,685.7674)"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
|
||||
clip-path="url(#clipPath21)" />
|
||||
<path
|
||||
id="path24"
|
||||
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.4480327,0,0,-1.2531805,622.20527,686.59525)"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
|
||||
clip-path="url(#clipPath25)" />
|
||||
<path
|
||||
id="path26"
|
||||
d="m 0,0 c -6.336,-9.247 -13.502,-17.869 -21.406,-25.773 -6.802,-6.793 -14.146,-13.063 -21.965,-18.718 -0.551,-0.411 -1.129,-0.812 -1.699,-1.204 -27.629,-19.549 -60.998,-31.53 -97.053,-32.976 -33.247,1.129 -64.852,8.762 -93.564,21.676 -13.054,7.082 -25.138,15.723 -36,25.661 -0.672,0.616 -1.343,1.232 -2.006,1.876 -1.288,1.194 -2.566,2.435 -3.816,3.685 -12.831,12.84 -23.748,27.593 -32.277,43.801 -1.092,7.651 -1.941,15.378 -2.445,23.039 -0.102,1.502 -0.186,3.004 -0.261,4.497 0,0 -2.865,29.795 23.944,36.634 26.827,6.84 65.654,19.745 87.722,50.305 0,0 0.327,-8.38 5.506,-15.779 8.034,-11.422 46.674,-100.46 46.674,-100.46 l 6.121,51.135 -13.978,15.079 -4.553,-1.987 15.228,16.544 c 0,0 8.94,4.218 16.554,-0.653 7.605,-4.899 14.146,-16.153 14.146,-16.153 l -5.879,3.892 -4.227,-12.364 c -0.756,-2.184 -0.83,-4.535 -0.233,-6.784 l 16.796,-62.687 c 0,0 -0.243,87.415 -2.781,111.685 0,0 14.221,-10.367 21.621,-14.454 7.381,-4.078 47.215,-20.407 53.738,-22.395 6.504,-1.987 13.222,-5.841 15.882,-11.916 1.026,-2.351 8.594,-26.939 17.486,-56.229 C -1.829,6.028 -0.914,3.023 0,0"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.4480327,0,0,-1.2531805,953.76928,974.41373)"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
|
||||
clip-path="url(#clipPath27)" />
|
||||
<path
|
||||
id="path28"
|
||||
d="m 0,0 c 0,0 -3.081,-67.22 -8.64,-90.616 -5.559,-23.397 30.316,0 30.316,0 l 20.9,68.629 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.4480327,0,0,-1.2531805,610.09146,775.07882)"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
|
||||
clip-path="url(#clipPath29)" />
|
||||
<path
|
||||
id="path30"
|
||||
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.4480327,0,0,-1.2531805,622.20527,686.59525)"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
|
||||
clip-path="url(#clipPath31)" />
|
||||
<path
|
||||
id="path32"
|
||||
d="m 0,0 c 0,0 12.178,7.604 15.029,18.355 0,0 36.913,-9.243 52.904,-26.719 C 66.887,-8.091 29.642,2.031 0,0 m -36.152,-5.643 c -26.791,0.404 -31.781,16.05 -34.086,29.523 -0.511,2.958 -3.148,5.073 -6.13,4.883 l -46.275,-4.764 14.922,7.14 c 3.505,1.687 7.164,3.077 10.895,4.17 3.742,1.093 7.556,1.901 11.429,2.376 4.051,0.499 8.542,1.01 13.46,1.509 0,0 6.19,2.459 7.414,-4.479 1.259,-6.926 0.499,-31.864 28.287,-31.103 27.777,0.76 32.612,19.412 32.612,19.412 0,0 3.458,14.471 9.232,14.257 5.263,-0.202 7.27,-6.142 7.853,-10.479 0.142,-1.045 -0.06,-3.54 0.582,-4.324 -0.939,1.152 -2.162,2.032 -3.564,2.554 C 7.604,26.113 3.065,26.945 2.471,21.658 1.616,13.877 -9.599,-6.059 -36.152,-5.643 m 13.306,48.354 9.528,-2.377 c 0,0 -2.388,-17.5 -11.227,-21.979 -0.25,-0.13 4.194,14.281 1.699,24.356 m 59.022,3.041 3.79,-1.687 c 0,0 2.198,-14.209 -3.79,-20.577 0,0 2.174,12.474 0,22.264 m -187.451,35.63 c 12.225,2.067 24.45,4.229 36.664,6.332 l 36.663,6.392 73.303,12.748 c 2.958,0.522 5.774,-1.462 6.285,-4.408 0.522,-2.947 -1.462,-5.762 -4.42,-6.285 -0.119,-0.024 -0.261,-0.036 -0.38,-0.047 H -3.184 L -114.243,85.029 c -12.344,-1.224 -24.676,-2.495 -37.032,-3.647 M 43.97,16.93 c 3.54,4.693 5.215,23.096 5.215,23.096 l 1.497,2.518 v 8.744 C 29.381,61.327 11.869,50.682 11.869,50.682 l -16.491,0.749 c -3.112,0.142 -7.449,2.637 -10.644,3.433 -4.42,1.093 -8.804,2.068 -13.342,2.566 -16.407,1.735 -32.933,0 -49.091,-2.934 -7.021,-1.271 -13.971,-2.851 -20.957,-4.325 -6.701,-1.425 -12.878,-3.908 -19.151,-6.605 -4.313,-1.842 -8.649,-3.659 -12.641,-6.119 -2.436,-1.496 -4.741,-3.243 -6.737,-5.31 -2.126,-2.21 -3.659,-4.859 -5.476,-7.319 -1.545,-2.091 -4.907,-0.463 -4.147,2.032 0.024,0.059 0.048,0.119 0.06,0.154 0.867,2.044 2.221,4.646 3.659,6.345 3.588,4.241 8.958,8.292 13.686,11.12 10.039,6.071 21.48,9.766 32.767,12.985 24.771,7.057 51.442,8.138 77.009,10.55 14.114,1.331 28.43,2.091 42.473,4.016 12.784,1.734 37.935,4.859 36.176,22.882 -1.71,18.129 -59.355,18.712 -59.355,18.712 0,0 -21.943,45.027 -27.372,50.468 -5.418,5.418 -16.503,18.474 -74.254,4.384 -57.739,-14.078 -55.327,-42.259 -55.327,-42.259 l -2.412,-39.622 c 0,0 -43.198,-11.179 -41.832,-23.321 1.391,-12.142 39.171,-12.819 39.171,-12.819 0,0 8.577,0.439 20.696,1.152 l -5.382,-9.683 c -6.749,-12.13 -6.38,-25.258 -8.851,-38.908 -4.277,-23.631 11.963,-52.702 33.978,-62.147 21.1,-9.053 53.949,-13.782 99.012,6.368 11.085,4.954 20.328,13.282 26.66,23.618 l 0.499,0.796 c 22.728,26.125 80.574,9.386 80.574,9.386 C 81.774,1.699 43.97,16.93 43.97,16.93"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.4480327,0,0,-1.2531805,836.82211,767.63192)"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
|
||||
clip-path="url(#clipPath33)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 13 KiB |
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;
|
||||
77
core/.golangci.yml
Normal file
77
core/.golangci.yml
Normal file
@@ -0,0 +1,77 @@
|
||||
linters-settings:
|
||||
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.File).WriteString
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- 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:
|
||||
157
core/Makefile
Normal file
157
core/Makefile
Normal file
@@ -0,0 +1,157 @@
|
||||
BINARY_NAME=dms
|
||||
BINARY_NAME_INSTALL=dankinstall
|
||||
SOURCE_DIR=cmd/dms
|
||||
SOURCE_DIR_INSTALL=cmd/dankinstall
|
||||
BUILD_DIR=bin
|
||||
PREFIX ?= /usr/local
|
||||
INSTALL_DIR=$(PREFIX)/bin
|
||||
|
||||
GO=go
|
||||
GOFLAGS=-ldflags="-s -w"
|
||||
|
||||
# Version and build info
|
||||
VERSION=$(shell git describe --tags --always 2>/dev/null || echo "dev")
|
||||
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)'
|
||||
|
||||
# Architecture to build for dist target (amd64, arm64, or all)
|
||||
ARCH ?= all
|
||||
|
||||
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps help
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
# Build the main binary (dms)
|
||||
build:
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||
|
||||
dankinstall:
|
||||
@echo "Building $(BINARY_NAME_INSTALL)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
||||
|
||||
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
||||
dist:
|
||||
ifeq ($(ARCH),all)
|
||||
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@echo "Building for linux/amd64..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
||||
@echo "Building for linux/arm64..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
||||
@echo "Distribution builds complete:"
|
||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
||||
else
|
||||
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@echo "Building for linux/$(ARCH)..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
|
||||
@echo "Distribution build complete:"
|
||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
|
||||
endif
|
||||
|
||||
build-all: build dankinstall
|
||||
|
||||
install: build
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Installation complete"
|
||||
|
||||
install-all: build-all
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||
@echo "Installation complete"
|
||||
|
||||
install-dankinstall: dankinstall
|
||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||
@echo "Installation complete"
|
||||
|
||||
uninstall:
|
||||
@echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Uninstall complete"
|
||||
|
||||
uninstall-all:
|
||||
@echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Uninstalling $(BINARY_NAME_INSTALL) from $(INSTALL_DIR)..."
|
||||
@rm -f $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||
@echo "Uninstall complete"
|
||||
|
||||
uninstall-dankinstall:
|
||||
@echo "Uninstalling $(BINARY_NAME_INSTALL) from $(INSTALL_DIR)..."
|
||||
@rm -f $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||
@echo "Uninstall complete"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@echo "Clean complete"
|
||||
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
$(GO) test -v ./...
|
||||
|
||||
fmt:
|
||||
@echo "Formatting Go code..."
|
||||
$(GO) fmt ./...
|
||||
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
$(GO) vet ./...
|
||||
|
||||
deps:
|
||||
@echo "Updating dependencies..."
|
||||
$(GO) mod tidy
|
||||
$(GO) mod download
|
||||
|
||||
dev:
|
||||
@echo "Building $(BINARY_NAME) for development..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||
@echo "Development build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||
|
||||
check-go:
|
||||
@echo "Checking Go version..."
|
||||
@go version | grep -E "go1\.(2[2-9]|[3-9][0-9])" > /dev/null || (echo "ERROR: Go 1.22 or higher required" && exit 1)
|
||||
@echo "Go version OK"
|
||||
|
||||
version: check-go
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Build Time: $(BUILD_TIME)"
|
||||
@echo "Commit: $(COMMIT)"
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Build the main binary (dms) (default)"
|
||||
@echo " build - Build the main binary (dms)"
|
||||
@echo " dankinstall - Build dankinstall binary"
|
||||
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
||||
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
||||
@echo " build-all - Build both binaries"
|
||||
@echo " install - Install dms to $(INSTALL_DIR)"
|
||||
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
||||
@echo " install-dankinstall - Install only dankinstall to $(INSTALL_DIR)"
|
||||
@echo " uninstall - Remove dms from $(INSTALL_DIR)"
|
||||
@echo " uninstall-all - Remove both binaries from $(INSTALL_DIR)"
|
||||
@echo " uninstall-dankinstall - Remove only dankinstall from $(INSTALL_DIR)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " test - Run tests"
|
||||
@echo " fmt - Format Go code"
|
||||
@echo " vet - Run go vet"
|
||||
@echo " deps - Update dependencies"
|
||||
@echo " dev - Build with debug info"
|
||||
@echo " check-go - Check Go version compatibility"
|
||||
@echo " version - Show version information"
|
||||
@echo " help - Show this help message"
|
||||
124
core/README.md
Normal file
124
core/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# DMS Backend & CLI
|
||||
|
||||
Go-based backend for DankMaterialShell providing system integration, IPC, and installation tools.
|
||||
|
||||
**See [root README](../README.md) for project overview and installation.**
|
||||
|
||||
## Components
|
||||
|
||||
**dms CLI**
|
||||
Command-line interface and daemon for shell management and system control.
|
||||
|
||||
**dankinstall**
|
||||
Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
|
||||
|
||||
## System Integration
|
||||
|
||||
**Wayland Protocols**
|
||||
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
||||
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
||||
- `ext-workspace-v1` - Workspace protocol support
|
||||
- `wlr-output-management-unstable-v1` - Display configuration
|
||||
|
||||
**DBus Interfaces**
|
||||
- NetworkManager/iwd - Network management
|
||||
- logind - Session control and inhibit locks
|
||||
- accountsservice - User account information
|
||||
- CUPS - Printer management
|
||||
- Custom IPC via unix socket (JSON API)
|
||||
|
||||
**Hardware Control**
|
||||
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
||||
- Backlight control - Internal display brightness via `login1` or sysfs
|
||||
- LED control - Keyboard/device LED management
|
||||
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
|
||||
|
||||
**Plugin System**
|
||||
- Plugin registry integration
|
||||
- Plugin lifecycle management
|
||||
- Settings persistence
|
||||
|
||||
## CLI Commands
|
||||
- `dms run [-d]` - Start shell (optionally as daemon)
|
||||
- `dms restart` / `dms kill` - Manage running processes
|
||||
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||
- `dms plugins [install|browse|search]` - Plugin management
|
||||
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
||||
|
||||
## Building
|
||||
|
||||
Requires Go 1.24+
|
||||
|
||||
**Development build:**
|
||||
```bash
|
||||
make # Build dms CLI
|
||||
make dankinstall # Build installer
|
||||
make test # Run tests
|
||||
```
|
||||
|
||||
**Distribution build:**
|
||||
```bash
|
||||
make dist # Build without update/greeter features
|
||||
```
|
||||
|
||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
sudo make install # Install to /usr/local/bin/dms
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
**Setup pre-commit hooks:**
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
||||
|
||||
**Regenerating Wayland Protocol Bindings:**
|
||||
```bash
|
||||
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
||||
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
||||
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
|
||||
```
|
||||
|
||||
**Module Structure:**
|
||||
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
||||
- `internal/distros/` - Distribution-specific installation logic
|
||||
- `internal/proto/` - Wayland protocol bindings
|
||||
- `pkg/` - Shared packages
|
||||
|
||||
## Installation via dankinstall
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.danklinux.com | sh
|
||||
```
|
||||
|
||||
## Supported Distributions
|
||||
|
||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
||||
|
||||
**Arch Linux**
|
||||
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
||||
|
||||
**Fedora**
|
||||
|
||||
Uses COPR repositories (`avengemedia/danklinux`, `avengemedia/dms`).
|
||||
|
||||
**Ubuntu**
|
||||
Requires PPA support. Most packages built from source (slow first install).
|
||||
|
||||
**Debian**
|
||||
Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
|
||||
|
||||
**openSUSE**
|
||||
Most packages available in standard repos. Minimal building required.
|
||||
|
||||
**Gentoo**
|
||||
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
||||
|
||||
See installer output for distribution-specific details during installation.
|
||||
46
core/assets/dank.svg
Normal file
46
core/assets/dank.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<svg viewBox="0 0 136 50" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- D -->
|
||||
<rect x="0" y="5" width="24" height="8" fill="#CCBEFF"/>
|
||||
<rect x="0" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="20" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="0" y="21" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="20" y="21" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="0" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="20" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="0" y="37" width="24" height="8" fill="#CCBEFF"/>
|
||||
|
||||
<!-- A -->
|
||||
<rect x="36" y="5" width="20" height="8" fill="#CCBEFF"/>
|
||||
<rect x="32" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="52" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="32" y="21" width="28" height="8" fill="#CCBEFF"/>
|
||||
<rect x="32" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="52" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="32" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="52" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||
|
||||
<!-- N -->
|
||||
<rect x="64" y="5" width="12" height="8" fill="#CCBEFF"/>
|
||||
<rect x="92" y="5" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="64" y="13" width="16" height="8" fill="#CCBEFF"/>
|
||||
<rect x="92" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="64" y="21" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="76" y="21" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="92" y="21" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="64" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="80" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="92" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="64" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="84" y="37" width="16" height="8" fill="#CCBEFF"/>
|
||||
|
||||
<!-- K -->
|
||||
<rect x="104" y="5" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="124" y="5" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="104" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="120" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="104" y="21" width="20" height="8" fill="#CCBEFF"/>
|
||||
<rect x="104" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="120" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="104" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||
<rect x="124" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
113
core/assets/danklogo.svg
Normal file
113
core/assets/danklogo.svg
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="482.90668"
|
||||
height="558.15088"
|
||||
viewBox="0 0 482.90667 558.15088"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
shape-rendering="auto"
|
||||
style="image-rendering: auto; filter: url(#smoothing);">
|
||||
<defs
|
||||
id="defs1">
|
||||
<filter id="smoothing" x="-0.05" y="-0.05" width="1.1" height="1.1">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="0.5" />
|
||||
</filter>
|
||||
<color-profile
|
||||
name="sRGB IEC61966-2.1"
|
||||
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
|
||||
id="color-profile1" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath21">
|
||||
<path
|
||||
d="M 0,1200 H 2000 V 0 H 0 Z"
|
||||
transform="translate(-673.87432,-704.25842)"
|
||||
id="path21" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath25">
|
||||
<path
|
||||
d="M 0,1200 H 2000 V 0 H 0 Z"
|
||||
transform="translate(-466.30451,-703.59782)"
|
||||
id="path25" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath27">
|
||||
<path
|
||||
d="M 0,1200 H 2000 V 0 H 0 Z"
|
||||
transform="translate(-695.28002,-473.92741)"
|
||||
id="path27" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath29">
|
||||
<path
|
||||
d="M 0,1200 H 2000 V 0 H 0 Z"
|
||||
transform="translate(-457.93881,-632.99062)"
|
||||
id="path29" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath31">
|
||||
<path
|
||||
d="M 0,1200 H 2000 V 0 H 0 Z"
|
||||
transform="translate(-466.30451,-703.59782)"
|
||||
id="path31" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath33">
|
||||
<path
|
||||
d="M 0,1200 H 2000 V 0 H 0 Z"
|
||||
transform="translate(-614.51722,-638.93302)"
|
||||
id="path33" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="layer-MC0"
|
||||
transform="translate(-486.31024,-515.02722)">
|
||||
<path
|
||||
id="path20"
|
||||
d="M 0,0 C -1.568,1.568 -3.163,3.098 -4.787,4.61 -5.944,3.966 -7.185,3.35 -8.529,2.762 -9.658,2.277 -10.815,1.82 -11.981,1.4 c 1.885,-1.689 3.742,-3.425 5.552,-5.207 0.448,-0.429 0.886,-0.868 1.325,-1.306 2.221,-2.221 4.386,-4.498 6.476,-6.84 27.639,-30.784 44.453,-71.459 44.453,-116.089 0,-29.347 -7.259,-56.977 -20.09,-81.21 -2.192,-4.134 -4.544,-8.174 -7.054,-12.102 -6.83,-10.74 -14.827,-20.669 -23.785,-29.636 -5.944,-5.944 -12.317,-11.459 -19.073,-16.498 -0.56,-0.42 -1.12,-0.83 -1.689,-1.231 -28.675,-20.893 -63.975,-33.201 -102.186,-33.201 -48.018,0 -91.464,19.456 -122.948,50.93 -0.737,0.737 -1.465,1.474 -2.174,2.221 -0.55,0.569 -1.101,1.147 -1.633,1.726 -15.545,16.553 -27.881,36.14 -36.018,57.779 -3.098,8.211 -5.58,16.712 -7.409,25.464 -2.417,11.534 -3.686,23.496 -3.686,35.758 0.01,42.326 15.117,81.097 40.246,111.246 -2.072,1.278 -3.975,2.809 -5.534,4.637 -26.174,-31.399 -41.934,-71.822 -41.934,-115.883 0,-18.187 2.678,-35.748 7.67,-52.311 3.359,-11.142 7.754,-21.835 13.092,-31.95 8.528,-16.208 19.446,-30.961 32.276,-43.801 1.251,-1.25 2.529,-2.491 3.817,-3.685 0.662,-0.644 1.334,-1.26 2.006,-1.876 10.862,-9.938 22.945,-18.578 36,-25.661 25.632,-13.913 55.016,-21.816 86.229,-21.816 2.454,0 4.908,0.047 7.334,0.14 36.056,1.446 69.424,13.427 97.054,32.976 0.569,0.392 1.148,0.793 1.698,1.204 7.82,5.655 15.164,11.925 21.966,18.718 7.904,7.904 15.07,16.526 21.406,25.773 2.556,3.723 4.973,7.558 7.25,11.477 15.499,26.697 24.382,57.723 24.382,90.812 C 53.038,-78.055 32.762,-32.762 0,0"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
|
||||
clip-path="url(#clipPath21)" />
|
||||
<path
|
||||
id="path24"
|
||||
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
|
||||
clip-path="url(#clipPath25)" />
|
||||
<path
|
||||
id="path26"
|
||||
d="m 0,0 c -6.336,-9.247 -13.502,-17.869 -21.406,-25.773 -6.802,-6.793 -14.146,-13.063 -21.965,-18.718 -0.551,-0.411 -1.129,-0.812 -1.699,-1.204 -27.629,-19.549 -60.998,-31.53 -97.053,-32.976 -33.247,1.129 -64.852,8.762 -93.564,21.676 -13.054,7.082 -25.138,15.723 -36,25.661 -0.672,0.616 -1.343,1.232 -2.006,1.876 -1.288,1.194 -2.566,2.435 -3.816,3.685 -12.831,12.84 -23.748,27.593 -32.277,43.801 -1.092,7.651 -1.941,15.378 -2.445,23.039 -0.102,1.502 -0.186,3.004 -0.261,4.497 0,0 -2.865,29.795 23.944,36.634 26.827,6.84 65.654,19.745 87.722,50.305 0,0 0.327,-8.38 5.506,-15.779 8.034,-11.422 46.674,-100.46 46.674,-100.46 l 6.121,51.135 -13.978,15.079 -4.553,-1.987 15.228,16.544 c 0,0 8.94,4.218 16.554,-0.653 7.605,-4.899 14.146,-16.153 14.146,-16.153 l -5.879,3.892 -4.227,-12.364 c -0.756,-2.184 -0.83,-4.535 -0.233,-6.784 l 16.796,-62.687 c 0,0 -0.243,87.415 -2.781,111.685 0,0 14.221,-10.367 21.621,-14.454 7.381,-4.078 47.215,-20.407 53.738,-22.395 6.504,-1.987 13.222,-5.841 15.882,-11.916 1.026,-2.351 8.594,-26.939 17.486,-56.229 C -1.829,6.028 -0.914,3.023 0,0"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
|
||||
clip-path="url(#clipPath27)" />
|
||||
<path
|
||||
id="path28"
|
||||
d="m 0,0 c 0,0 -3.081,-67.22 -8.64,-90.616 -5.559,-23.397 30.316,0 30.316,0 l 20.9,68.629 z"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
|
||||
clip-path="url(#clipPath29)" />
|
||||
<path
|
||||
id="path30"
|
||||
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
|
||||
clip-path="url(#clipPath31)" />
|
||||
<path
|
||||
id="path32"
|
||||
d="m 0,0 c 0,0 12.178,7.604 15.029,18.355 0,0 36.913,-9.243 52.904,-26.719 C 66.887,-8.091 29.642,2.031 0,0 m -36.152,-5.643 c -26.791,0.404 -31.781,16.05 -34.086,29.523 -0.511,2.958 -3.148,5.073 -6.13,4.883 l -46.275,-4.764 14.922,7.14 c 3.505,1.687 7.164,3.077 10.895,4.17 3.742,1.093 7.556,1.901 11.429,2.376 4.051,0.499 8.542,1.01 13.46,1.509 0,0 6.19,2.459 7.414,-4.479 1.259,-6.926 0.499,-31.864 28.287,-31.103 27.777,0.76 32.612,19.412 32.612,19.412 0,0 3.458,14.471 9.232,14.257 5.263,-0.202 7.27,-6.142 7.853,-10.479 0.142,-1.045 -0.06,-3.54 0.582,-4.324 -0.939,1.152 -2.162,2.032 -3.564,2.554 C 7.604,26.113 3.065,26.945 2.471,21.658 1.616,13.877 -9.599,-6.059 -36.152,-5.643 m 13.306,48.354 9.528,-2.377 c 0,0 -2.388,-17.5 -11.227,-21.979 -0.25,-0.13 4.194,14.281 1.699,24.356 m 59.022,3.041 3.79,-1.687 c 0,0 2.198,-14.209 -3.79,-20.577 0,0 2.174,12.474 0,22.264 m -187.451,35.63 c 12.225,2.067 24.45,4.229 36.664,6.332 l 36.663,6.392 73.303,12.748 c 2.958,0.522 5.774,-1.462 6.285,-4.408 0.522,-2.947 -1.462,-5.762 -4.42,-6.285 -0.119,-0.024 -0.261,-0.036 -0.38,-0.047 H -3.184 L -114.243,85.029 c -12.344,-1.224 -24.676,-2.495 -37.032,-3.647 M 43.97,16.93 c 3.54,4.693 5.215,23.096 5.215,23.096 l 1.497,2.518 v 8.744 C 29.381,61.327 11.869,50.682 11.869,50.682 l -16.491,0.749 c -3.112,0.142 -7.449,2.637 -10.644,3.433 -4.42,1.093 -8.804,2.068 -13.342,2.566 -16.407,1.735 -32.933,0 -49.091,-2.934 -7.021,-1.271 -13.971,-2.851 -20.957,-4.325 -6.701,-1.425 -12.878,-3.908 -19.151,-6.605 -4.313,-1.842 -8.649,-3.659 -12.641,-6.119 -2.436,-1.496 -4.741,-3.243 -6.737,-5.31 -2.126,-2.21 -3.659,-4.859 -5.476,-7.319 -1.545,-2.091 -4.907,-0.463 -4.147,2.032 0.024,0.059 0.048,0.119 0.06,0.154 0.867,2.044 2.221,4.646 3.659,6.345 3.588,4.241 8.958,8.292 13.686,11.12 10.039,6.071 21.48,9.766 32.767,12.985 24.771,7.057 51.442,8.138 77.009,10.55 14.114,1.331 28.43,2.091 42.473,4.016 12.784,1.734 37.935,4.859 36.176,22.882 -1.71,18.129 -59.355,18.712 -59.355,18.712 0,0 -21.943,45.027 -27.372,50.468 -5.418,5.418 -16.503,18.474 -74.254,4.384 -57.739,-14.078 -55.327,-42.259 -55.327,-42.259 l -2.412,-39.622 c 0,0 -43.198,-11.179 -41.832,-23.321 1.391,-12.142 39.171,-12.819 39.171,-12.819 0,0 8.577,0.439 20.696,1.152 l -5.382,-9.683 c -6.749,-12.13 -6.38,-25.258 -8.851,-38.908 -4.277,-23.631 11.963,-52.702 33.978,-62.147 21.1,-9.053 53.949,-13.782 99.012,6.368 11.085,4.954 20.328,13.282 26.66,23.618 l 0.499,0.796 c 22.728,26.125 80.574,9.386 80.574,9.386 C 81.774,1.699 43.97,16.93 43.97,16.93"
|
||||
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
|
||||
clip-path="url(#clipPath33)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
38
core/build_dankinstall.sh
Executable file
38
core/build_dankinstall.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Get latest version tag
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||
|
||||
echo -e "${GREEN}Building dankinstall ${VERSION}${NC}"
|
||||
|
||||
# Create bin directory if it doesn't exist
|
||||
mkdir -p bin
|
||||
|
||||
# Build for each architecture
|
||||
for ARCH in amd64 arm64; do
|
||||
echo -e "${BLUE}Building for ${ARCH}...${NC}"
|
||||
|
||||
cd cmd/dankinstall
|
||||
GOOS=linux CGO_ENABLED=0 GOARCH=${ARCH} \
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${VERSION}" \
|
||||
-o ../../bin/dankinstall-${ARCH}
|
||||
cd ../..
|
||||
|
||||
# Compress
|
||||
gzip -9 -k -f bin/dankinstall-${ARCH}
|
||||
|
||||
# Generate checksum
|
||||
(cd bin && sha256sum dankinstall-${ARCH}.gz > dankinstall-${ARCH}.gz.sha256)
|
||||
|
||||
echo -e "${GREEN}✓ Built bin/dankinstall-${ARCH}.gz${NC}"
|
||||
done
|
||||
|
||||
echo -e "${GREEN}Done! Files ready in bin/:${NC}"
|
||||
ls -lh bin/dankinstall-*
|
||||
50
core/cmd/dankinstall/main.go
Normal file
50
core/cmd/dankinstall/main.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
func main() {
|
||||
fileLogger, err := log.NewFileLogger()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
||||
fmt.Println("Continuing without file logging...")
|
||||
}
|
||||
|
||||
logFilePath := ""
|
||||
if fileLogger != nil {
|
||||
logFilePath = fileLogger.GetLogPath()
|
||||
fmt.Printf("Logging to: %s\n", logFilePath)
|
||||
defer func() {
|
||||
if err := fileLogger.Close(); err != nil {
|
||||
fmt.Printf("Warning: Failed to close log file: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
model := tui.NewModel(Version, logFilePath)
|
||||
|
||||
if fileLogger != nil {
|
||||
fileLogger.StartListening(model.GetLogChan())
|
||||
}
|
||||
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("Error running program: %v\n", err)
|
||||
if logFilePath != "" {
|
||||
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if logFilePath != "" {
|
||||
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
||||
}
|
||||
}
|
||||
303
core/cmd/dms/commands_brightness.go
Normal file
303
core/cmd/dms/commands_brightness.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var brightnessCmd = &cobra.Command{
|
||||
Use: "brightness",
|
||||
Short: "Control device brightness",
|
||||
Long: "Control brightness for backlight and LED devices (use --ddc to include DDC/I2C monitors)",
|
||||
}
|
||||
|
||||
var brightnessListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all brightness devices",
|
||||
Long: "List all available brightness devices with their current values",
|
||||
Run: runBrightnessList,
|
||||
}
|
||||
|
||||
var brightnessSetCmd = &cobra.Command{
|
||||
Use: "set <device_id> <percent>",
|
||||
Short: "Set brightness for a device",
|
||||
Long: "Set brightness percentage (0-100) for a specific device",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: runBrightnessSet,
|
||||
}
|
||||
|
||||
var brightnessGetCmd = &cobra.Command{
|
||||
Use: "get <device_id>",
|
||||
Short: "Get brightness for a device",
|
||||
Long: "Get current brightness percentage for a specific device",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runBrightnessGet,
|
||||
}
|
||||
|
||||
func init() {
|
||||
brightnessListCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)")
|
||||
brightnessSetCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)")
|
||||
brightnessSetCmd.Flags().Bool("exponential", false, "Use exponential brightness scaling")
|
||||
brightnessSetCmd.Flags().Float64("exponent", 1.2, "Exponent for exponential scaling (default 1.2)")
|
||||
brightnessGetCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)")
|
||||
|
||||
brightnessCmd.SetHelpTemplate(`{{.Long}}
|
||||
|
||||
Usage:
|
||||
{{.UseLine}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
||||
|
||||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
||||
{{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
||||
`)
|
||||
|
||||
brightnessListCmd.SetHelpTemplate(`{{.Long}}
|
||||
|
||||
Usage:
|
||||
{{.UseLine}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
||||
`)
|
||||
|
||||
brightnessSetCmd.SetHelpTemplate(`{{.Long}}
|
||||
|
||||
Usage:
|
||||
{{.UseLine}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
||||
`)
|
||||
|
||||
brightnessGetCmd.SetHelpTemplate(`{{.Long}}
|
||||
|
||||
Usage:
|
||||
{{.UseLine}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
|
||||
`)
|
||||
|
||||
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
|
||||
}
|
||||
|
||||
func runBrightnessList(cmd *cobra.Command, args []string) {
|
||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||
|
||||
allDevices := []brightness.Device{}
|
||||
|
||||
sysfs, err := brightness.NewSysfsBackend()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to initialize sysfs backend: %v", err)
|
||||
} else {
|
||||
devices, err := sysfs.GetDevices()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to get sysfs devices: %v", err)
|
||||
} else {
|
||||
allDevices = append(allDevices, devices...)
|
||||
}
|
||||
}
|
||||
|
||||
if includeDDC {
|
||||
ddc, err := brightness.NewDDCBackend()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to initialize DDC backend: %v\n", err)
|
||||
} else {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
devices, err := ddc.GetDevices()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to get DDC devices: %v\n", err)
|
||||
} else {
|
||||
allDevices = append(allDevices, devices...)
|
||||
}
|
||||
ddc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if len(allDevices) == 0 {
|
||||
fmt.Println("No brightness devices found")
|
||||
return
|
||||
}
|
||||
|
||||
maxIDLen := len("Device")
|
||||
maxNameLen := len("Name")
|
||||
for _, dev := range allDevices {
|
||||
if len(dev.ID) > maxIDLen {
|
||||
maxIDLen = len(dev.ID)
|
||||
}
|
||||
if len(dev.Name) > maxNameLen {
|
||||
maxNameLen = len(dev.Name)
|
||||
}
|
||||
}
|
||||
|
||||
idPad := maxIDLen + 2
|
||||
namePad := maxNameLen + 2
|
||||
|
||||
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
||||
|
||||
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
||||
for i := 0; i < sepLen; i++ {
|
||||
fmt.Print("─")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
for _, device := range allDevices {
|
||||
fmt.Printf("%-*s %-12s %-*s %3d%%\n",
|
||||
idPad,
|
||||
device.ID,
|
||||
device.Class,
|
||||
namePad,
|
||||
device.Name,
|
||||
device.CurrentPercent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func runBrightnessSet(cmd *cobra.Command, args []string) {
|
||||
deviceID := args[0]
|
||||
var percent int
|
||||
if _, err := fmt.Sscanf(args[1], "%d", &percent); err != nil {
|
||||
log.Fatalf("Invalid percent value: %s", args[1])
|
||||
}
|
||||
|
||||
if percent < 0 || percent > 100 {
|
||||
log.Fatalf("Percent must be between 0 and 100")
|
||||
}
|
||||
|
||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||
exponential, _ := cmd.Flags().GetBool("exponential")
|
||||
exponent, _ := cmd.Flags().GetFloat64("exponent")
|
||||
|
||||
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
|
||||
parts := strings.SplitN(deviceID, ":", 2)
|
||||
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
|
||||
subsystem := parts[0]
|
||||
name := parts[1]
|
||||
|
||||
// Initialize backends needed for logind approach
|
||||
sysfs, err := brightness.NewSysfsBackend()
|
||||
if err != nil {
|
||||
log.Debugf("NewSysfsBackend failed: %v", err)
|
||||
} else {
|
||||
logind, err := brightness.NewLogindBackend()
|
||||
if err != nil {
|
||||
log.Debugf("NewLogindBackend failed: %v", err)
|
||||
} else {
|
||||
defer logind.Close()
|
||||
|
||||
// Get device info to convert percent to value
|
||||
dev, err := sysfs.GetDevice(deviceID)
|
||||
if err == nil {
|
||||
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
|
||||
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
|
||||
|
||||
// Call logind with hardware value
|
||||
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
|
||||
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
|
||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||
return
|
||||
} else {
|
||||
log.Debugf("logind.SetBrightness failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct sysfs (requires write permissions)
|
||||
sysfs, err := brightness.NewSysfsBackend()
|
||||
if err == nil {
|
||||
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
|
||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||
return
|
||||
}
|
||||
log.Debugf("sysfs.SetBrightness failed: %v", err)
|
||||
} else {
|
||||
log.Debugf("NewSysfsBackend failed: %v", err)
|
||||
}
|
||||
|
||||
// Try DDC if requested
|
||||
if includeDDC {
|
||||
ddc, err := brightness.NewDDCBackend()
|
||||
if err == nil {
|
||||
defer ddc.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||
return
|
||||
}
|
||||
log.Debugf("ddc.SetBrightness failed: %v", err)
|
||||
} else {
|
||||
log.Debugf("NewDDCBackend failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
||||
}
|
||||
|
||||
func runBrightnessGet(cmd *cobra.Command, args []string) {
|
||||
deviceID := args[0]
|
||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||
|
||||
allDevices := []brightness.Device{}
|
||||
|
||||
sysfs, err := brightness.NewSysfsBackend()
|
||||
if err == nil {
|
||||
devices, err := sysfs.GetDevices()
|
||||
if err == nil {
|
||||
allDevices = append(allDevices, devices...)
|
||||
}
|
||||
}
|
||||
|
||||
if includeDDC {
|
||||
ddc, err := brightness.NewDDCBackend()
|
||||
if err == nil {
|
||||
defer ddc.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
devices, err := ddc.GetDevices()
|
||||
if err == nil {
|
||||
allDevices = append(allDevices, devices...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, device := range allDevices {
|
||||
if device.ID == deviceID {
|
||||
fmt.Printf("%s: %d%% (%d/%d)\n",
|
||||
device.ID,
|
||||
device.CurrentPercent,
|
||||
device.Current,
|
||||
device.Max,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Fatalf("Device not found: %s", deviceID)
|
||||
}
|
||||
377
core/cmd/dms/commands_common.go
Normal file
377
core/cmd/dms/commands_common.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show version information",
|
||||
Run: runVersion,
|
||||
}
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Launch quickshell with DMS configuration",
|
||||
Long: "Launch quickshell with DMS configuration (qs -c dms)",
|
||||
PreRunE: findConfig,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
daemon, _ := cmd.Flags().GetBool("daemon")
|
||||
session, _ := cmd.Flags().GetBool("session")
|
||||
if daemon {
|
||||
runShellDaemon(session)
|
||||
} else {
|
||||
runShellInteractive(session)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var restartCmd = &cobra.Command{
|
||||
Use: "restart",
|
||||
Short: "Restart quickshell with DMS configuration",
|
||||
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
|
||||
PreRunE: findConfig,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
restartShell()
|
||||
},
|
||||
}
|
||||
|
||||
var restartDetachedCmd = &cobra.Command{
|
||||
Use: "restart-detached <pid>",
|
||||
Hidden: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: findConfig,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runDetachedRestart(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
var killCmd = &cobra.Command{
|
||||
Use: "kill",
|
||||
Short: "Kill running DMS shell processes",
|
||||
Long: "Kill all running quickshell processes with DMS configuration",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
killShell()
|
||||
},
|
||||
}
|
||||
|
||||
var ipcCmd = &cobra.Command{
|
||||
Use: "ipc",
|
||||
Short: "Send IPC commands to running DMS shell",
|
||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
||||
PreRunE: findConfig,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runShellIPCCommand(args)
|
||||
},
|
||||
}
|
||||
|
||||
var debugSrvCmd = &cobra.Command{
|
||||
Use: "debug-srv",
|
||||
Short: "Start the debug server",
|
||||
Long: "Start the Unix socket debug server for DMS",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := startDebugServer(); err != nil {
|
||||
log.Fatalf("Error starting debug server: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var pluginsCmd = &cobra.Command{
|
||||
Use: "plugins",
|
||||
Short: "Manage DMS plugins",
|
||||
Long: "Browse and manage DMS plugins from the registry",
|
||||
}
|
||||
|
||||
var pluginsBrowseCmd = &cobra.Command{
|
||||
Use: "browse",
|
||||
Short: "Browse available plugins",
|
||||
Long: "Browse available plugins from the DMS plugin registry",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := browsePlugins(); err != nil {
|
||||
log.Fatalf("Error browsing plugins: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var pluginsListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List installed plugins",
|
||||
Long: "List all installed DMS plugins",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := listInstalledPlugins(); err != nil {
|
||||
log.Fatalf("Error listing plugins: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var pluginsInstallCmd = &cobra.Command{
|
||||
Use: "install <plugin-id>",
|
||||
Short: "Install a plugin by ID",
|
||||
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := installPluginCLI(args[0]); err != nil {
|
||||
log.Fatalf("Error installing plugin: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var pluginsUninstallCmd = &cobra.Command{
|
||||
Use: "uninstall <plugin-id>",
|
||||
Short: "Uninstall a plugin by ID",
|
||||
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := uninstallPluginCLI(args[0]); err != nil {
|
||||
log.Fatalf("Error uninstalling plugin: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func runVersion(cmd *cobra.Command, args []string) {
|
||||
printASCII()
|
||||
fmt.Printf("%s\n", Version)
|
||||
}
|
||||
|
||||
func startDebugServer() error {
|
||||
server.CLIVersion = Version
|
||||
return server.Start(true)
|
||||
}
|
||||
|
||||
func browsePlugins() error {
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create registry: %w", err)
|
||||
}
|
||||
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create manager: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Fetching plugin registry...")
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list plugins: %w", err)
|
||||
}
|
||||
|
||||
if len(pluginList) == 0 {
|
||||
fmt.Println("No plugins found in registry.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList))
|
||||
for _, plugin := range pluginList {
|
||||
installed, _ := manager.IsInstalled(plugin)
|
||||
installedMarker := ""
|
||||
if installed {
|
||||
installedMarker = " [Installed]"
|
||||
}
|
||||
|
||||
fmt.Printf(" %s%s\n", plugin.Name, installedMarker)
|
||||
fmt.Printf(" ID: %s\n", plugin.ID)
|
||||
fmt.Printf(" Category: %s\n", plugin.Category)
|
||||
fmt.Printf(" Author: %s\n", plugin.Author)
|
||||
fmt.Printf(" Description: %s\n", plugin.Description)
|
||||
fmt.Printf(" Repository: %s\n", plugin.Repo)
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
fmt.Printf(" Capabilities: %s\n", strings.Join(plugin.Capabilities, ", "))
|
||||
}
|
||||
if len(plugin.Compositors) > 0 {
|
||||
fmt.Printf(" Compositors: %s\n", strings.Join(plugin.Compositors, ", "))
|
||||
}
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", "))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listInstalledPlugins() error {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create manager: %w", err)
|
||||
}
|
||||
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create registry: %w", err)
|
||||
}
|
||||
|
||||
installedNames, err := manager.ListInstalled()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list installed plugins: %w", err)
|
||||
}
|
||||
|
||||
if len(installedNames) == 0 {
|
||||
fmt.Println("No plugins installed.")
|
||||
return nil
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list plugins: %w", err)
|
||||
}
|
||||
|
||||
pluginMap := make(map[string]plugins.Plugin)
|
||||
for _, p := range allPlugins {
|
||||
pluginMap[p.ID] = p
|
||||
}
|
||||
|
||||
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
|
||||
for _, id := range installedNames {
|
||||
if plugin, ok := pluginMap[id]; ok {
|
||||
fmt.Printf(" %s\n", plugin.Name)
|
||||
fmt.Printf(" ID: %s\n", plugin.ID)
|
||||
fmt.Printf(" Category: %s\n", plugin.Category)
|
||||
fmt.Printf(" Author: %s\n", plugin.Author)
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Printf(" %s (not in registry)\n\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installPluginCLI(idOrName string) error {
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create registry: %w", err)
|
||||
}
|
||||
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create manager: %w", err)
|
||||
}
|
||||
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list plugins: %w", err)
|
||||
}
|
||||
|
||||
// First, try to find by ID (preferred method)
|
||||
var plugin *plugins.Plugin
|
||||
for _, p := range pluginList {
|
||||
if p.ID == idOrName {
|
||||
plugin = &p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to name for backward compatibility
|
||||
if plugin == nil {
|
||||
for _, p := range pluginList {
|
||||
if p.Name == idOrName {
|
||||
plugin = &p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("plugin not found: %s", idOrName)
|
||||
}
|
||||
|
||||
installed, err := manager.IsInstalled(*plugin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check install status: %w", err)
|
||||
}
|
||||
|
||||
if installed {
|
||||
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
fmt.Printf("Installing plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||
if err := manager.Install(*plugin); err != nil {
|
||||
return fmt.Errorf("failed to install plugin: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Plugin installed successfully: %s\n", plugin.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallPluginCLI(idOrName string) error {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create manager: %w", err)
|
||||
}
|
||||
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create registry: %w", err)
|
||||
}
|
||||
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list plugins: %w", err)
|
||||
}
|
||||
|
||||
// First, try to find by ID (preferred method)
|
||||
var plugin *plugins.Plugin
|
||||
for _, p := range pluginList {
|
||||
if p.ID == idOrName {
|
||||
plugin = &p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to name for backward compatibility
|
||||
if plugin == nil {
|
||||
for _, p := range pluginList {
|
||||
if p.Name == idOrName {
|
||||
plugin = &p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("plugin not found: %s", idOrName)
|
||||
}
|
||||
|
||||
installed, err := manager.IsInstalled(*plugin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check install status: %w", err)
|
||||
}
|
||||
|
||||
if !installed {
|
||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||
if err := manager.Uninstall(*plugin); err != nil {
|
||||
return fmt.Errorf("failed to uninstall plugin: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCommonCommands returns the commands available in all builds
|
||||
func getCommonCommands() []*cobra.Command {
|
||||
return []*cobra.Command{
|
||||
versionCmd,
|
||||
runCmd,
|
||||
restartCmd,
|
||||
restartDetachedCmd,
|
||||
killCmd,
|
||||
ipcCmd,
|
||||
debugSrvCmd,
|
||||
pluginsCmd,
|
||||
dank16Cmd,
|
||||
brightnessCmd,
|
||||
dpmsCmd,
|
||||
keybindsCmd,
|
||||
greeterCmd,
|
||||
setupCmd,
|
||||
}
|
||||
}
|
||||
80
core/cmd/dms/commands_dank16.go
Normal file
80
core/cmd/dms/commands_dank16.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dank16Cmd = &cobra.Command{
|
||||
Use: "dank16 <hex_color>",
|
||||
Short: "Generate Base16 color palettes",
|
||||
Long: "Generate Base16 color palettes from a color with support for various output formats",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runDank16,
|
||||
}
|
||||
|
||||
func init() {
|
||||
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant")
|
||||
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
||||
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
||||
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||
dank16Cmd.Flags().String("background", "", "Custom background color")
|
||||
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
||||
}
|
||||
|
||||
func runDank16(cmd *cobra.Command, args []string) {
|
||||
primaryColor := args[0]
|
||||
if !strings.HasPrefix(primaryColor, "#") {
|
||||
primaryColor = "#" + primaryColor
|
||||
}
|
||||
|
||||
isLight, _ := cmd.Flags().GetBool("light")
|
||||
isJson, _ := cmd.Flags().GetBool("json")
|
||||
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||
isFoot, _ := cmd.Flags().GetBool("foot")
|
||||
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
||||
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
||||
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||
background, _ := cmd.Flags().GetString("background")
|
||||
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
||||
|
||||
if background != "" && !strings.HasPrefix(background, "#") {
|
||||
background = "#" + background
|
||||
}
|
||||
|
||||
contrastAlgo = strings.ToLower(contrastAlgo)
|
||||
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
|
||||
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
|
||||
}
|
||||
|
||||
opts := dank16.PaletteOptions{
|
||||
IsLight: isLight,
|
||||
Background: background,
|
||||
UseDPS: contrastAlgo == "dps",
|
||||
}
|
||||
|
||||
colors := dank16.GeneratePalette(primaryColor, opts)
|
||||
|
||||
if isJson {
|
||||
fmt.Print(dank16.GenerateJSON(colors))
|
||||
} else if isKitty {
|
||||
fmt.Print(dank16.GenerateKittyTheme(colors))
|
||||
} else if isFoot {
|
||||
fmt.Print(dank16.GenerateFootTheme(colors))
|
||||
} else if isAlacritty {
|
||||
fmt.Print(dank16.GenerateAlacrittyTheme(colors))
|
||||
} else if isGhostty {
|
||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||
} else if isWezterm {
|
||||
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
||||
} else {
|
||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||
}
|
||||
}
|
||||
84
core/cmd/dms/commands_dpms.go
Normal file
84
core/cmd/dms/commands_dpms.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dpmsCmd = &cobra.Command{
|
||||
Use: "dpms",
|
||||
Short: "Control display power management",
|
||||
}
|
||||
|
||||
var dpmsOnCmd = &cobra.Command{
|
||||
Use: "on [output]",
|
||||
Short: "Turn display(s) on",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: runDPMSOn,
|
||||
}
|
||||
|
||||
var dpmsOffCmd = &cobra.Command{
|
||||
Use: "off [output]",
|
||||
Short: "Turn display(s) off",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: runDPMSOff,
|
||||
}
|
||||
|
||||
var dpmsListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List outputs",
|
||||
Args: cobra.NoArgs,
|
||||
Run: runDPMSList,
|
||||
}
|
||||
|
||||
func init() {
|
||||
dpmsCmd.AddCommand(dpmsOnCmd, dpmsOffCmd, dpmsListCmd)
|
||||
}
|
||||
|
||||
func runDPMSOn(cmd *cobra.Command, args []string) {
|
||||
outputName := ""
|
||||
if len(args) > 0 {
|
||||
outputName = args[0]
|
||||
}
|
||||
|
||||
client, err := newDPMSClient()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.SetDPMS(outputName, true); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runDPMSOff(cmd *cobra.Command, args []string) {
|
||||
outputName := ""
|
||||
if len(args) > 0 {
|
||||
outputName = args[0]
|
||||
}
|
||||
|
||||
client, err := newDPMSClient()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.SetDPMS(outputName, false); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runDPMSList(cmd *cobra.Command, args []string) {
|
||||
client, err := newDPMSClient()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
for _, output := range client.ListOutputs() {
|
||||
fmt.Println(output)
|
||||
}
|
||||
}
|
||||
465
core/cmd/dms/commands_features.go
Normal file
465
core/cmd/dms/commands_features.go
Normal file
@@ -0,0 +1,465 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update DankMaterialShell to the latest version",
|
||||
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
|
||||
PreRunE: findConfig,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runUpdate()
|
||||
},
|
||||
}
|
||||
|
||||
var updateCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check if updates are available for DankMaterialShell",
|
||||
Long: "Check for available updates without performing the actual update",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runUpdateCheck()
|
||||
},
|
||||
}
|
||||
|
||||
func runUpdateCheck() {
|
||||
fmt.Println("Checking for DankMaterialShell updates...")
|
||||
fmt.Println()
|
||||
|
||||
versionInfo, err := version.GetDMSVersionInfo()
|
||||
if err != nil {
|
||||
log.Fatalf("Error checking for updates: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Current version: %s\n", versionInfo.Current)
|
||||
fmt.Printf("Latest version: %s\n", versionInfo.Latest)
|
||||
fmt.Println()
|
||||
|
||||
if versionInfo.HasUpdate {
|
||||
fmt.Println("✓ Update available!")
|
||||
fmt.Println()
|
||||
fmt.Println("Run 'dms update' to install the latest version.")
|
||||
os.Exit(0)
|
||||
} else {
|
||||
fmt.Println("✓ You are running the latest version.")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func runUpdate() {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
log.Fatalf("Error detecting OS: %v", err)
|
||||
}
|
||||
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
log.Fatalf("Unsupported distribution: %s", osInfo.Distribution.ID)
|
||||
}
|
||||
|
||||
var updateErr error
|
||||
switch config.Family {
|
||||
case distros.FamilyArch:
|
||||
updateErr = updateArchLinux()
|
||||
case distros.FamilySUSE:
|
||||
updateErr = updateOtherDistros()
|
||||
default:
|
||||
updateErr = updateOtherDistros()
|
||||
}
|
||||
|
||||
if updateErr != nil {
|
||||
if errors.Is(updateErr, errdefs.ErrUpdateCancelled) {
|
||||
log.Info("Update cancelled.")
|
||||
return
|
||||
}
|
||||
if errors.Is(updateErr, errdefs.ErrNoUpdateNeeded) {
|
||||
return
|
||||
}
|
||||
log.Fatalf("Error updating DMS: %v", updateErr)
|
||||
}
|
||||
|
||||
log.Info("Update complete! Restarting DMS...")
|
||||
restartShell()
|
||||
}
|
||||
|
||||
func updateArchLinux() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
|
||||
if _, err := os.Stat(dmsPath); err == nil {
|
||||
return updateOtherDistros()
|
||||
}
|
||||
}
|
||||
|
||||
var packageName string
|
||||
if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
} else if isArchPackageInstalled("dms-shell-git") {
|
||||
packageName = "dms-shell-git"
|
||||
} else {
|
||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
return updateOtherDistros()
|
||||
}
|
||||
|
||||
var helper string
|
||||
var updateCmd *exec.Cmd
|
||||
|
||||
if commandExists("yay") {
|
||||
helper = "yay"
|
||||
updateCmd = exec.Command("yay", "-S", packageName)
|
||||
} else if commandExists("paru") {
|
||||
helper = "paru"
|
||||
updateCmd = exec.Command("paru", "-S", packageName)
|
||||
} else {
|
||||
fmt.Println("Error: Neither yay nor paru found - please install an AUR helper")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
return updateOtherDistros()
|
||||
}
|
||||
|
||||
fmt.Printf("This will update DankMaterialShell using %s.\n", helper)
|
||||
if !confirmUpdate() {
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
fmt.Printf("\nRunning: %s -S %s\n", helper, packageName)
|
||||
updateCmd.Stdout = os.Stdout
|
||||
updateCmd.Stderr = os.Stderr
|
||||
err = updateCmd.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: Failed to update using %s: %v\n", helper, err)
|
||||
}
|
||||
|
||||
fmt.Println("dms successfully updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateOtherDistros() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
|
||||
|
||||
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("DMS configuration directory not found at %s", dmsPath)
|
||||
}
|
||||
|
||||
fmt.Printf("Found DMS configuration at %s\n", dmsPath)
|
||||
|
||||
versionInfo, err := version.GetDMSVersionInfo()
|
||||
if err == nil && !versionInfo.HasUpdate {
|
||||
fmt.Println()
|
||||
fmt.Printf("Current version: %s\n", versionInfo.Current)
|
||||
fmt.Printf("Latest version: %s\n", versionInfo.Latest)
|
||||
fmt.Println()
|
||||
fmt.Println("✓ You are already running the latest version.")
|
||||
return errdefs.ErrNoUpdateNeeded
|
||||
}
|
||||
|
||||
fmt.Println("\nThis will update:")
|
||||
fmt.Println(" 1. The dms binary from GitHub releases")
|
||||
fmt.Println(" 2. DankMaterialShell configuration using git")
|
||||
if !confirmUpdate() {
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Updating dms binary ===")
|
||||
if err := updateDMSBinary(); err != nil {
|
||||
fmt.Printf("Warning: Failed to update dms binary: %v\n", err)
|
||||
fmt.Println("Continuing with shell configuration update...")
|
||||
} else {
|
||||
fmt.Println("dms binary successfully updated")
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Updating DMS shell configuration ===")
|
||||
|
||||
if err := os.Chdir(dmsPath); err != nil {
|
||||
return fmt.Errorf("failed to change to DMS directory: %w", err)
|
||||
}
|
||||
|
||||
statusCmd := exec.Command("git", "status", "--porcelain")
|
||||
statusOutput, _ := statusCmd.Output()
|
||||
hasLocalChanges := len(strings.TrimSpace(string(statusOutput))) > 0
|
||||
|
||||
currentRefCmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
|
||||
currentRefOutput, _ := currentRefCmd.Output()
|
||||
onBranch := len(currentRefOutput) > 0
|
||||
|
||||
var currentTag string
|
||||
var currentBranch string
|
||||
|
||||
if !onBranch {
|
||||
tagCmd := exec.Command("git", "describe", "--exact-match", "--tags", "HEAD")
|
||||
if tagOutput, err := tagCmd.Output(); err == nil {
|
||||
currentTag = strings.TrimSpace(string(tagOutput))
|
||||
}
|
||||
} else {
|
||||
branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
if branchOutput, err := branchCmd.Output(); err == nil {
|
||||
currentBranch = strings.TrimSpace(string(branchOutput))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Fetching latest changes...")
|
||||
fetchCmd := exec.Command("git", "fetch", "origin", "--tags", "--force")
|
||||
fetchCmd.Stdout = os.Stdout
|
||||
fetchCmd.Stderr = os.Stderr
|
||||
if err := fetchCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to fetch changes: %w", err)
|
||||
}
|
||||
|
||||
if currentTag != "" {
|
||||
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
||||
latestTagOutput, err := latestTagCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest tag: %w", err)
|
||||
}
|
||||
|
||||
tags := strings.Split(strings.TrimSpace(string(latestTagOutput)), "\n")
|
||||
if len(tags) == 0 || tags[0] == "" {
|
||||
return fmt.Errorf("no version tags found")
|
||||
}
|
||||
latestTag := tags[0]
|
||||
|
||||
if latestTag == currentTag {
|
||||
fmt.Printf("Already on latest tag: %s\n", currentTag)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Current tag: %s\n", currentTag)
|
||||
fmt.Printf("Latest tag: %s\n", latestTag)
|
||||
|
||||
if hasLocalChanges {
|
||||
fmt.Println("\nWarning: You have local changes in your DMS configuration.")
|
||||
if offerReclone(dmsPath) {
|
||||
return nil
|
||||
}
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
fmt.Printf("Updating to %s...\n", latestTag)
|
||||
checkoutCmd := exec.Command("git", "checkout", latestTag)
|
||||
checkoutCmd.Stdout = os.Stdout
|
||||
checkoutCmd.Stderr = os.Stderr
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
fmt.Printf("Error: Failed to checkout %s: %v\n", latestTag, err)
|
||||
if offerReclone(dmsPath) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("update cancelled")
|
||||
}
|
||||
|
||||
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentBranch == "" {
|
||||
currentBranch = "master"
|
||||
}
|
||||
|
||||
fmt.Printf("Current branch: %s\n", currentBranch)
|
||||
|
||||
if hasLocalChanges {
|
||||
fmt.Println("\nWarning: You have local changes in your DMS configuration.")
|
||||
if offerReclone(dmsPath) {
|
||||
return nil
|
||||
}
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
pullCmd := exec.Command("git", "pull", "origin", currentBranch)
|
||||
pullCmd.Stdout = os.Stdout
|
||||
pullCmd.Stderr = os.Stderr
|
||||
if err := pullCmd.Run(); err != nil {
|
||||
fmt.Printf("Error: Failed to pull latest changes: %v\n", err)
|
||||
if offerReclone(dmsPath) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("update cancelled")
|
||||
}
|
||||
|
||||
fmt.Println("\nUpdate complete!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func offerReclone(dmsPath string) bool {
|
||||
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil || !strings.HasPrefix(strings.ToLower(strings.TrimSpace(response)), "y") {
|
||||
return false
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
backupPath := fmt.Sprintf("%s.backup-%d", dmsPath, timestamp)
|
||||
|
||||
fmt.Printf("Backing up current directory to %s...\n", backupPath)
|
||||
if err := os.Rename(dmsPath, backupPath); err != nil {
|
||||
fmt.Printf("Error: Failed to backup directory: %v\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Println("Cloning fresh copy...")
|
||||
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
fmt.Printf("Error: Failed to clone repository: %v\n", err)
|
||||
fmt.Printf("Restoring backup...\n")
|
||||
os.Rename(backupPath, dmsPath)
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully re-cloned repository (backup at %s)\n", backupPath)
|
||||
return true
|
||||
}
|
||||
|
||||
func confirmUpdate() bool {
|
||||
fmt.Print("Do you want to proceed with the update? (y/N): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading input: %v\n", err)
|
||||
return false
|
||||
}
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
return response == "y" || response == "yes"
|
||||
}
|
||||
|
||||
func updateDMSBinary() error {
|
||||
arch := ""
|
||||
switch strings.ToLower(os.Getenv("HOSTTYPE")) {
|
||||
case "x86_64", "amd64":
|
||||
arch = "amd64"
|
||||
case "aarch64", "arm64":
|
||||
arch = "arm64"
|
||||
default:
|
||||
cmd := exec.Command("uname", "-m")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect architecture: %w", err)
|
||||
}
|
||||
archStr := strings.TrimSpace(string(output))
|
||||
switch archStr {
|
||||
case "x86_64":
|
||||
arch = "amd64"
|
||||
case "aarch64":
|
||||
arch = "arm64"
|
||||
default:
|
||||
return fmt.Errorf("unsupported architecture: %s", archStr)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Fetching latest release version...")
|
||||
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch latest release: %w", err)
|
||||
}
|
||||
|
||||
version := ""
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
if strings.Contains(line, "\"tag_name\"") {
|
||||
parts := strings.Split(line, "\"")
|
||||
if len(parts) >= 4 {
|
||||
version = parts[3]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
return fmt.Errorf("could not determine latest version")
|
||||
}
|
||||
|
||||
fmt.Printf("Latest version: %s\n", version)
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "dms-update-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
binaryURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
|
||||
checksumURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz.sha256", version, arch)
|
||||
|
||||
binaryPath := filepath.Join(tempDir, "dms.gz")
|
||||
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
|
||||
|
||||
fmt.Println("Downloading dms binary...")
|
||||
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to download binary: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Downloading checksum...")
|
||||
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to download checksum: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Verifying checksum...")
|
||||
checksumData, err := os.ReadFile(checksumPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read checksum file: %w", err)
|
||||
}
|
||||
expectedChecksum := strings.Fields(string(checksumData))[0]
|
||||
|
||||
actualCmd := exec.Command("sha256sum", binaryPath)
|
||||
actualOutput, err := actualCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
actualChecksum := strings.Fields(string(actualOutput))[0]
|
||||
|
||||
if expectedChecksum != actualChecksum {
|
||||
return fmt.Errorf("checksum verification failed\nExpected: %s\nGot: %s", expectedChecksum, actualChecksum)
|
||||
}
|
||||
|
||||
fmt.Println("Decompressing binary...")
|
||||
decompressCmd := exec.Command("gunzip", binaryPath)
|
||||
if err := decompressCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to decompress binary: %w", err)
|
||||
}
|
||||
|
||||
decompressedPath := filepath.Join(tempDir, "dms")
|
||||
|
||||
if err := os.Chmod(decompressedPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||
}
|
||||
|
||||
currentPath, err := exec.LookPath("dms")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find current dms binary: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Installing to %s...\n", currentPath)
|
||||
|
||||
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
||||
replaceCmd.Stdin = os.Stdin
|
||||
replaceCmd.Stdout = os.Stdout
|
||||
replaceCmd.Stderr = os.Stderr
|
||||
if err := replaceCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to replace binary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
720
core/cmd/dms/commands_greeter.go
Normal file
720
core/cmd/dms/commands_greeter.go
Normal file
@@ -0,0 +1,720 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var greeterCmd = &cobra.Command{
|
||||
Use: "greeter",
|
||||
Short: "Manage DMS greeter",
|
||||
Long: "Manage DMS greeter (greetd)",
|
||||
}
|
||||
|
||||
var greeterInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install and configure DMS greeter",
|
||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := installGreeter(); err != nil {
|
||||
log.Fatalf("Error installing greeter: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var greeterSyncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Sync DMS theme and settings with greeter",
|
||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := syncGreeter(); err != nil {
|
||||
log.Fatalf("Error syncing greeter: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var greeterEnableCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable DMS greeter in greetd config",
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := enableGreeter(); err != nil {
|
||||
log.Fatalf("Error enabling greeter: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var greeterStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check greeter sync status",
|
||||
Long: "Check the status of greeter installation and configuration sync",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := checkGreeterStatus(); err != nil {
|
||||
log.Fatalf("Error checking greeter status: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func installGreeter() error {
|
||||
fmt.Println("=== DMS Greeter Installation ===")
|
||||
|
||||
logFunc := func(msg string) {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
|
||||
if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nDetecting DMS installation...")
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||
|
||||
fmt.Println("\nDetecting installed compositors...")
|
||||
compositors := greeter.DetectCompositors()
|
||||
if len(compositors) == 0 {
|
||||
return fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
||||
}
|
||||
|
||||
var selectedCompositor string
|
||||
if len(compositors) == 1 {
|
||||
selectedCompositor = compositors[0]
|
||||
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
|
||||
} else {
|
||||
var err error
|
||||
selectedCompositor, err = greeter.PromptCompositorChoice(compositors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up dms-greeter group and permissions...")
|
||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nCopying greeter files...")
|
||||
if err := greeter.CopyGreeterFiles(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguring greetd...")
|
||||
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Installation Complete ===")
|
||||
fmt.Println("\nTo test the greeter, run:")
|
||||
fmt.Println(" sudo systemctl start greetd")
|
||||
fmt.Println("\nTo enable on boot, run:")
|
||||
fmt.Println(" sudo systemctl enable --now greetd")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncGreeter() error {
|
||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
||||
fmt.Println()
|
||||
|
||||
logFunc := func(msg string) {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
|
||||
fmt.Println("Detecting DMS installation...")
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
||||
}
|
||||
|
||||
greeterGroupExists := checkGroupExists("greeter")
|
||||
if greeterGroupExists {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
|
||||
groupsCmd := exec.Command("groups", currentUser.Username)
|
||||
groupsOutput, err := groupsCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check groups: %w", err)
|
||||
}
|
||||
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||
if !inGreeterGroup {
|
||||
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
|
||||
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
|
||||
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response == "y" || response == "yes" {
|
||||
fmt.Println("\nAdding user to greeter group...")
|
||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
|
||||
addUserCmd.Stdout = os.Stdout
|
||||
addUserCmd.Stderr = os.Stderr
|
||||
if err := addUserCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to add user to greeter group: %w", err)
|
||||
}
|
||||
fmt.Println("✓ User added to greeter group")
|
||||
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up permissions and ACLs...")
|
||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Sync Complete ===")
|
||||
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
||||
fmt.Println("The changes will be visible on the next login screen.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkGroupExists(groupName string) bool {
|
||||
data, err := os.ReadFile("/etc/group")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, groupName+":") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func disableDisplayManager(dmName string) (bool, error) {
|
||||
state, err := getSystemdServiceState(dmName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check %s state: %w", dmName, err)
|
||||
}
|
||||
|
||||
if !state.Exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nChecking %s...\n", dmName)
|
||||
fmt.Printf(" Current state: enabled=%s\n", state.EnabledState)
|
||||
|
||||
actionTaken := false
|
||||
|
||||
if state.NeedsDisable {
|
||||
var disableCmd *exec.Cmd
|
||||
var actionVerb string
|
||||
|
||||
if state.EnabledState == "static" {
|
||||
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
||||
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
|
||||
actionVerb = "masked"
|
||||
} else {
|
||||
fmt.Printf(" Disabling %s...\n", dmName)
|
||||
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
|
||||
actionVerb = "disabled"
|
||||
}
|
||||
|
||||
disableCmd.Stdout = os.Stdout
|
||||
disableCmd.Stderr = os.Stderr
|
||||
if err := disableCmd.Run(); err != nil {
|
||||
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
||||
}
|
||||
|
||||
enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName)
|
||||
if verifyErr != nil {
|
||||
fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr)
|
||||
} else if shouldDisable {
|
||||
return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState)
|
||||
}
|
||||
|
||||
actionTaken = true
|
||||
} else {
|
||||
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||
fmt.Printf(" ✓ %s is already masked\n", dmName)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %s is already disabled\n", dmName)
|
||||
}
|
||||
}
|
||||
|
||||
return actionTaken, nil
|
||||
}
|
||||
|
||||
func ensureGreetdEnabled() error {
|
||||
fmt.Println("\nChecking greetd service status...")
|
||||
|
||||
state, err := getSystemdServiceState("greetd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check greetd state: %w", err)
|
||||
}
|
||||
|
||||
if !state.Exists {
|
||||
return fmt.Errorf("greetd service not found. Please install greetd first")
|
||||
}
|
||||
|
||||
fmt.Printf(" Current state: %s\n", state.EnabledState)
|
||||
|
||||
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||
fmt.Println(" Unmasking greetd...")
|
||||
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
|
||||
unmaskCmd.Stdout = os.Stdout
|
||||
unmaskCmd.Stderr = os.Stderr
|
||||
if err := unmaskCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||
}
|
||||
fmt.Println(" ✓ Unmasked greetd")
|
||||
}
|
||||
|
||||
switch state.EnabledState {
|
||||
case "disabled", "masked", "masked-runtime":
|
||||
fmt.Println(" Enabling greetd service...")
|
||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
|
||||
enableCmd.Stdout = os.Stdout
|
||||
enableCmd.Stderr = os.Stderr
|
||||
if err := enableCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
fmt.Println(" ✓ Enabled greetd service")
|
||||
case "enabled", "enabled-runtime":
|
||||
fmt.Println(" ✓ greetd is already enabled")
|
||||
default:
|
||||
fmt.Printf(" ℹ greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureGraphicalTarget() error {
|
||||
getDefaultCmd := exec.Command("systemctl", "get-default")
|
||||
currentTarget, err := getDefaultCmd.Output()
|
||||
if err != nil {
|
||||
fmt.Println("⚠ Warning: Could not detect current default systemd target")
|
||||
return nil
|
||||
}
|
||||
|
||||
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
||||
if currentTargetStr != "graphical.target" {
|
||||
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
||||
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
|
||||
setDefaultCmd.Stdout = os.Stdout
|
||||
setDefaultCmd.Stderr = os.Stderr
|
||||
if err := setDefaultCmd.Run(); err != nil {
|
||||
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
||||
fmt.Println(" Greeter may not start on boot. Run manually:")
|
||||
fmt.Println(" sudo systemctl set-default graphical.target")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("✓ Set graphical.target as default")
|
||||
} else {
|
||||
fmt.Println("✓ Default target already set to graphical.target")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConflictingDisplayManagers() error {
|
||||
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
||||
|
||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
|
||||
|
||||
disabledAny := false
|
||||
var errors []string
|
||||
|
||||
for _, dm := range conflictingDMs {
|
||||
actionTaken, err := disableDisplayManager(dm)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err)
|
||||
errors = append(errors, errMsg)
|
||||
fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg)
|
||||
continue
|
||||
}
|
||||
if actionTaken {
|
||||
disabledAny = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
|
||||
fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║")
|
||||
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
||||
fmt.Println("\nSome display managers could not be disabled:")
|
||||
for _, err := range errors {
|
||||
fmt.Printf(" ✗ %s\n", err)
|
||||
}
|
||||
fmt.Println("\nThis may prevent greetd from starting properly.")
|
||||
fmt.Println("You may need to manually disable them before greetd will work.")
|
||||
fmt.Println("\nManual commands to try:")
|
||||
for _, dm := range conflictingDMs {
|
||||
fmt.Printf(" sudo systemctl disable %s\n", dm)
|
||||
fmt.Printf(" sudo systemctl mask %s\n", dm)
|
||||
}
|
||||
fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ")
|
||||
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response == "n" || response == "no" {
|
||||
return fmt.Errorf("aborted due to display manager conflicts")
|
||||
}
|
||||
fmt.Println("\nContinuing despite errors...")
|
||||
}
|
||||
|
||||
if !disabledAny && len(errors) == 0 {
|
||||
fmt.Println("\n✓ No conflicting display managers found")
|
||||
} else if disabledAny && len(errors) == 0 {
|
||||
fmt.Println("\n✓ Successfully handled all conflicting display managers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableGreeter() error {
|
||||
fmt.Println("=== DMS Greeter Enable ===")
|
||||
fmt.Println()
|
||||
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read greetd config: %w", err)
|
||||
}
|
||||
|
||||
configContent := string(data)
|
||||
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
||||
|
||||
if configAlreadyCorrect {
|
||||
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleConflictingDisplayManagers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGreetdEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Enable Complete ===")
|
||||
fmt.Println("\nGreeter configuration verified and system state corrected.")
|
||||
fmt.Println("To start the greeter now, run:")
|
||||
fmt.Println(" sudo systemctl start greetd")
|
||||
fmt.Println("\nOr reboot to see the greeter at boot time.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Detecting installed compositors...")
|
||||
compositors := greeter.DetectCompositors()
|
||||
|
||||
if commandExists("sway") {
|
||||
compositors = append(compositors, "sway")
|
||||
}
|
||||
|
||||
if len(compositors) == 0 {
|
||||
return fmt.Errorf("no supported compositors found (niri, Hyprland, or sway required)")
|
||||
}
|
||||
|
||||
var selectedCompositor string
|
||||
if len(compositors) == 1 {
|
||||
selectedCompositor = compositors[0]
|
||||
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
|
||||
} else {
|
||||
var err error
|
||||
selectedCompositor, err = promptCompositorChoice(compositors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
||||
}
|
||||
|
||||
backupPath := configPath + ".backup"
|
||||
backupCmd := exec.Command("sudo", "cp", configPath, backupPath)
|
||||
if err := backupCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to backup config: %w", err)
|
||||
}
|
||||
fmt.Printf("✓ Backed up config to %s\n", backupPath)
|
||||
|
||||
lines := strings.Split(configContent, "\n")
|
||||
var newLines []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
wrapperCmd, err := findCommandPath("dms-greeter")
|
||||
if err != nil {
|
||||
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
|
||||
}
|
||||
|
||||
compositorLower := strings.ToLower(selectedCompositor)
|
||||
commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
|
||||
|
||||
var finalLines []string
|
||||
inDefaultSession := false
|
||||
commandAdded := false
|
||||
|
||||
for _, line := range newLines {
|
||||
finalLines = append(finalLines, line)
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if trimmed == "[default_session]" {
|
||||
inDefaultSession = true
|
||||
}
|
||||
|
||||
if inDefaultSession && !commandAdded {
|
||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||
finalLines = append(finalLines, commandLine)
|
||||
commandAdded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !commandAdded {
|
||||
finalLines = append(finalLines, commandLine)
|
||||
}
|
||||
|
||||
newConfig := strings.Join(finalLines, "\n")
|
||||
|
||||
tmpFile := "/tmp/greetd-config.toml"
|
||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write temp config: %w", err)
|
||||
}
|
||||
|
||||
moveCmd := exec.Command("sudo", "mv", tmpFile, configPath)
|
||||
if err := moveCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to update config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleConflictingDisplayManagers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGreetdEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Enable Complete ===")
|
||||
fmt.Println("\nTo start the greeter now, run:")
|
||||
fmt.Println(" sudo systemctl start greetd")
|
||||
fmt.Println("\nOr reboot to see the greeter at boot time.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptCompositorChoice(compositors []string) (string, error) {
|
||||
fmt.Println("\nMultiple compositors detected:")
|
||||
for i, comp := range compositors {
|
||||
fmt.Printf("%d) %s\n", i+1, comp)
|
||||
}
|
||||
|
||||
var response string
|
||||
fmt.Print("Choose compositor for greeter: ")
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
choice := 0
|
||||
fmt.Sscanf(response, "%d", &choice)
|
||||
|
||||
if choice < 1 || choice > len(compositors) {
|
||||
return "", fmt.Errorf("invalid choice")
|
||||
}
|
||||
|
||||
return compositors[choice-1], nil
|
||||
}
|
||||
|
||||
func checkGreeterStatus() error {
|
||||
fmt.Println("=== DMS Greeter Status ===")
|
||||
fmt.Println()
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
fmt.Println("Greeter Configuration:")
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
configContent := string(data)
|
||||
if strings.Contains(configContent, "dms-greeter") {
|
||||
lines := strings.Split(configContent, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||
fmt.Println(" ✓ Greeter is enabled")
|
||||
|
||||
if strings.Contains(command, "--command niri") {
|
||||
fmt.Println(" Compositor: niri")
|
||||
} else if strings.Contains(command, "--command hyprland") {
|
||||
fmt.Println(" Compositor: Hyprland")
|
||||
} else if strings.Contains(command, "--command sway") {
|
||||
fmt.Println(" Compositor: sway")
|
||||
} else {
|
||||
fmt.Println(" Compositor: unknown")
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ✗ Greeter is NOT enabled")
|
||||
fmt.Println(" Run 'dms greeter enable' to enable it")
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ✗ Greeter config not found")
|
||||
fmt.Println(" Run 'dms greeter install' to install greeter")
|
||||
}
|
||||
|
||||
fmt.Println("\nGroup Membership:")
|
||||
groupsCmd := exec.Command("groups", currentUser.Username)
|
||||
groupsOutput, err := groupsCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check groups: %w", err)
|
||||
}
|
||||
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||
if inGreeterGroup {
|
||||
fmt.Println(" ✓ User is in greeter group")
|
||||
} else {
|
||||
fmt.Println(" ✗ User is NOT in greeter group")
|
||||
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
|
||||
}
|
||||
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
fmt.Println("\nGreeter Cache Directory:")
|
||||
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||
} else {
|
||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||
fmt.Println(" Run 'dms greeter install' to create cache directory")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguration Symlinks:")
|
||||
symlinks := []struct {
|
||||
source string
|
||||
target string
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"),
|
||||
target: filepath.Join(cacheDir, "settings.json"),
|
||||
desc: "Settings",
|
||||
},
|
||||
{
|
||||
source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"),
|
||||
target: filepath.Join(cacheDir, "session.json"),
|
||||
desc: "Session state",
|
||||
},
|
||||
{
|
||||
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||
target: filepath.Join(cacheDir, "colors.json"),
|
||||
desc: "Color theme",
|
||||
},
|
||||
}
|
||||
|
||||
allGood := true
|
||||
for _, link := range symlinks {
|
||||
targetInfo, err := os.Lstat(link.target)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ %s: symlink not found at %s\n", link.desc, link.target)
|
||||
allGood = false
|
||||
continue
|
||||
}
|
||||
|
||||
if targetInfo.Mode()&os.ModeSymlink == 0 {
|
||||
fmt.Printf(" ✗ %s: %s is not a symlink\n", link.desc, link.target)
|
||||
allGood = false
|
||||
continue
|
||||
}
|
||||
|
||||
linkDest, err := os.Readlink(link.target)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ %s: failed to read symlink\n", link.desc)
|
||||
allGood = false
|
||||
continue
|
||||
}
|
||||
|
||||
if linkDest != link.source {
|
||||
fmt.Printf(" ✗ %s: symlink points to wrong location\n", link.desc)
|
||||
fmt.Printf(" Expected: %s\n", link.source)
|
||||
fmt.Printf(" Got: %s\n", linkDest)
|
||||
allGood = false
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||
fmt.Printf(" ⚠ %s: symlink OK, but source file doesn't exist yet\n", link.desc)
|
||||
fmt.Printf(" Will be created when you run DMS\n")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if allGood && inGreeterGroup {
|
||||
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||
} else if !allGood {
|
||||
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to fix symlinks.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
143
core/cmd/dms/commands_keybinds.go
Normal file
143
core/cmd/dms/commands_keybinds.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var keybindsCmd = &cobra.Command{
|
||||
Use: "keybinds",
|
||||
Aliases: []string{"cheatsheet", "chsht"},
|
||||
Short: "Manage keybinds and cheatsheets",
|
||||
Long: "Display and manage keybinds and cheatsheets for various applications",
|
||||
}
|
||||
|
||||
var keybindsListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List available providers",
|
||||
Long: "List all available keybind/cheatsheet providers",
|
||||
Run: runKeybindsList,
|
||||
}
|
||||
|
||||
var keybindsShowCmd = &cobra.Command{
|
||||
Use: "show <provider>",
|
||||
Short: "Show keybinds for a provider",
|
||||
Long: "Display keybinds/cheatsheet for the specified provider",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runKeybindsShow,
|
||||
}
|
||||
|
||||
func init() {
|
||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||
|
||||
keybindsCmd.AddCommand(keybindsListCmd)
|
||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||
|
||||
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||
return providers.NewJSONFileProvider(filePath)
|
||||
})
|
||||
|
||||
initializeProviders()
|
||||
}
|
||||
|
||||
func initializeProviders() {
|
||||
registry := keybinds.GetDefaultRegistry()
|
||||
|
||||
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
|
||||
if err := registry.Register(hyprlandProvider); err != nil {
|
||||
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||
}
|
||||
|
||||
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
|
||||
if err := registry.Register(mangowcProvider); err != nil {
|
||||
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||
}
|
||||
|
||||
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
||||
if err := registry.Register(swayProvider); err != nil {
|
||||
log.Warnf("Failed to register Sway provider: %v", err)
|
||||
}
|
||||
|
||||
niriProvider := providers.NewNiriProvider("")
|
||||
if err := registry.Register(niriProvider); err != nil {
|
||||
log.Warnf("Failed to register Niri provider: %v", err)
|
||||
}
|
||||
|
||||
config := keybinds.DefaultDiscoveryConfig()
|
||||
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
|
||||
log.Warnf("Failed to auto-discover providers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runKeybindsList(cmd *cobra.Command, args []string) {
|
||||
registry := keybinds.GetDefaultRegistry()
|
||||
providers := registry.List()
|
||||
|
||||
if len(providers) == 0 {
|
||||
fmt.Fprintln(os.Stdout, "No providers available")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||
for _, name := range providers {
|
||||
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
registry := keybinds.GetDefaultRegistry()
|
||||
|
||||
customPath, _ := cmd.Flags().GetString("path")
|
||||
if customPath != "" {
|
||||
var provider keybinds.Provider
|
||||
switch providerName {
|
||||
case "hyprland":
|
||||
provider = providers.NewHyprlandProvider(customPath)
|
||||
case "mangowc":
|
||||
provider = providers.NewMangoWCProvider(customPath)
|
||||
case "sway":
|
||||
provider = providers.NewSwayProvider(customPath)
|
||||
case "niri":
|
||||
provider = providers.NewNiriProvider(customPath)
|
||||
default:
|
||||
log.Fatalf("Provider %s does not support custom path", providerName)
|
||||
}
|
||||
|
||||
sheet, err := provider.GetCheatSheet()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting cheatsheet: %v", err)
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(sheet, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Error generating JSON: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := registry.Get(providerName)
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
sheet, err := provider.GetCheatSheet()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting cheatsheet: %v", err)
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(sheet, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Error generating JSON: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
224
core/cmd/dms/commands_open.go
Normal file
224
core/cmd/dms/commands_open.go
Normal file
@@ -0,0 +1,224 @@
|
||||
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)")
|
||||
}
|
||||
|
||||
// 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]interface{}{
|
||||
"target": actualTarget,
|
||||
}
|
||||
|
||||
if detectedMimeType != "" {
|
||||
params["mimeType"] = detectedMimeType
|
||||
}
|
||||
|
||||
if len(detectedCategories) > 0 {
|
||||
params["categories"] = detectedCategories
|
||||
}
|
||||
|
||||
if detectedRequestType != "" {
|
||||
params["requestType"] = detectedRequestType
|
||||
}
|
||||
|
||||
method := "apppicker.open"
|
||||
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
|
||||
method = "browser.open"
|
||||
params["url"] = target
|
||||
}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
|
||||
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||
log.Fatalf("Failed to send request: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Request sent successfully")
|
||||
}
|
||||
90
core/cmd/dms/commands_root.go
Normal file
90
core/cmd/dms/commands_root.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var customConfigPath string
|
||||
var configPath string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "dms",
|
||||
Short: "dms CLI",
|
||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||
Run: runInteractiveMode,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add the -c flag
|
||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||
}
|
||||
|
||||
func findConfig(cmd *cobra.Command, args []string) error {
|
||||
if customConfigPath != "" {
|
||||
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
|
||||
shellPath := filepath.Join(customConfigPath, "shell.qml")
|
||||
|
||||
info, statErr := os.Stat(shellPath)
|
||||
|
||||
if statErr == nil && !info.IsDir() {
|
||||
configPath = customConfigPath
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil // <-- Guard statement
|
||||
}
|
||||
|
||||
if statErr != nil {
|
||||
return fmt.Errorf("custom config path error: %w", statErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
|
||||
}
|
||||
|
||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
||||
statePath := strings.TrimSpace(string(data))
|
||||
shellPath := filepath.Join(statePath, "shell.qml")
|
||||
|
||||
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
||||
log.Debug("Using config from active session state file: %s", statePath)
|
||||
configPath = statePath
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil // <-- Guard statement
|
||||
} else {
|
||||
os.Remove(configStateFile)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("No custom path or active session, searching default XDG locations...")
|
||||
var err error
|
||||
configPath, err = config.LocateDMSConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
}
|
||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
||||
detector, _ := dms.NewDetector()
|
||||
|
||||
if !detector.IsDMSInstalled() {
|
||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
||||
log.Info("Please install DMS using dankinstall before using this management interface.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
model := dms.NewModel(Version)
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
log.Fatalf("Error running program: %v", err)
|
||||
}
|
||||
}
|
||||
182
core/cmd/dms/commands_setup.go
Normal file
182
core/cmd/dms/commands_setup.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
log.Fatalf("Error during setup: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func runSetup() error {
|
||||
fmt.Println("=== DMS Configuration Setup ===")
|
||||
|
||||
wm, wmSelected := promptCompositor()
|
||||
terminal, terminalSelected := promptTerminal()
|
||||
|
||||
if !wmSelected && !terminalSelected {
|
||||
fmt.Println("No configurations selected. Exiting.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if wmSelected || terminalSelected {
|
||||
willBackup := checkExistingConfigs(wm, wmSelected, terminal, terminalSelected)
|
||||
if willBackup {
|
||||
fmt.Println("\n⚠ Existing configurations will be backed up with timestamps.")
|
||||
}
|
||||
|
||||
fmt.Print("\nProceed with deployment? (y/N): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Println("Setup cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nDeploying configurations...")
|
||||
logChan := make(chan string, 100)
|
||||
deployer := config.NewConfigDeployer(logChan)
|
||||
|
||||
go func() {
|
||||
for msg := range logChan {
|
||||
fmt.Println(" " + msg)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
var results []config.DeploymentResult
|
||||
var err error
|
||||
|
||||
if wmSelected && terminalSelected {
|
||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
|
||||
} else if wmSelected {
|
||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
if len(results) > 1 {
|
||||
results = results[:1]
|
||||
}
|
||||
} else if terminalSelected {
|
||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
|
||||
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
||||
results = results[1:]
|
||||
}
|
||||
}
|
||||
|
||||
close(logChan)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("deployment failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Deployment Complete ===")
|
||||
for _, result := range results {
|
||||
if result.Deployed {
|
||||
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
|
||||
if result.BackupPath != "" {
|
||||
fmt.Printf(" Backup: %s\n", result.BackupPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptCompositor() (deps.WindowManager, bool) {
|
||||
fmt.Println("Select compositor:")
|
||||
fmt.Println("1) Niri")
|
||||
fmt.Println("2) Hyprland")
|
||||
fmt.Println("3) None")
|
||||
|
||||
var response string
|
||||
fmt.Print("\nChoice (1-3): ")
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
switch response {
|
||||
case "1":
|
||||
return deps.WindowManagerNiri, true
|
||||
case "2":
|
||||
return deps.WindowManagerHyprland, true
|
||||
default:
|
||||
return deps.WindowManagerNiri, false
|
||||
}
|
||||
}
|
||||
|
||||
func promptTerminal() (deps.Terminal, bool) {
|
||||
fmt.Println("\nSelect terminal:")
|
||||
fmt.Println("1) Ghostty")
|
||||
fmt.Println("2) Kitty")
|
||||
fmt.Println("3) Alacritty")
|
||||
fmt.Println("4) None")
|
||||
|
||||
var response string
|
||||
fmt.Print("\nChoice (1-4): ")
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
switch response {
|
||||
case "1":
|
||||
return deps.TerminalGhostty, true
|
||||
case "2":
|
||||
return deps.TerminalKitty, true
|
||||
case "3":
|
||||
return deps.TerminalAlacritty, true
|
||||
default:
|
||||
return deps.TerminalGhostty, false
|
||||
}
|
||||
}
|
||||
|
||||
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
|
||||
homeDir := os.Getenv("HOME")
|
||||
willBackup := false
|
||||
|
||||
if wmSelected {
|
||||
var configPath string
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
|
||||
case deps.WindowManagerHyprland:
|
||||
configPath = filepath.Join(homeDir, ".config", "hypr", "hyprland.conf")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
willBackup = true
|
||||
}
|
||||
}
|
||||
|
||||
if terminalSelected {
|
||||
var configPath string
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
configPath = filepath.Join(homeDir, ".config", "ghostty", "config")
|
||||
case deps.TerminalKitty:
|
||||
configPath = filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
|
||||
case deps.TerminalAlacritty:
|
||||
configPath = filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
willBackup = true
|
||||
}
|
||||
}
|
||||
|
||||
return willBackup
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
44
core/cmd/dms/main.go
Normal file
44
core/cmd/dms/main.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
func init() {
|
||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to update
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
41
core/cmd/dms/main_distro.go
Normal file
41
core/cmd/dms/main_distro.go
Normal file
@@ -0,0 +1,41 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
func init() {
|
||||
// Add flags
|
||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Block root
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
496
core/cmd/dms/shell.go
Normal file
496
core/cmd/dms/shell.go
Normal file
@@ -0,0 +1,496 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
)
|
||||
|
||||
var isSessionManaged bool
|
||||
|
||||
func execDetachedRestart(targetPID int) {
|
||||
selfPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
|
||||
func runDetachedRestart(targetPIDStr string) {
|
||||
targetPID, err := strconv.Atoi(targetPIDStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
proc, err := os.FindProcess(targetPID)
|
||||
if err == nil {
|
||||
proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
killShell()
|
||||
runShellDaemon(false)
|
||||
}
|
||||
|
||||
func getRuntimeDir() string {
|
||||
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
||||
return runtime
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
func hasSystemdRun() bool {
|
||||
_, err := exec.LookPath("systemd-run")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func getPIDFilePath() string {
|
||||
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
|
||||
}
|
||||
|
||||
func writePIDFile(childPID int) error {
|
||||
pidFile := getPIDFilePath()
|
||||
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0644)
|
||||
}
|
||||
|
||||
func removePIDFile() {
|
||||
pidFile := getPIDFilePath()
|
||||
os.Remove(pidFile)
|
||||
}
|
||||
|
||||
func getAllDMSPIDs() []int {
|
||||
dir := getRuntimeDir()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pids []int
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||
continue
|
||||
}
|
||||
|
||||
pidFile := filepath.Join(dir, entry.Name())
|
||||
data, err := os.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
os.Remove(pidFile)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the child process is still alive
|
||||
proc, err := os.FindProcess(childPID)
|
||||
if err != nil {
|
||||
os.Remove(pidFile)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||
// Process is dead, remove stale PID file
|
||||
os.Remove(pidFile)
|
||||
continue
|
||||
}
|
||||
|
||||
pids = append(pids, childPID)
|
||||
|
||||
// Also get the parent PID from the filename
|
||||
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
||||
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
||||
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
||||
// Check if parent is still alive
|
||||
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
||||
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
||||
pids = append(pids, parentPID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pids
|
||||
}
|
||||
|
||||
func runShellInteractive(session bool) {
|
||||
isSessionManaged = session
|
||||
go printASCII()
|
||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
socketPath := server.GetSocketPath()
|
||||
|
||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
|
||||
log.Warnf("Failed to write config state file: %v", err)
|
||||
}
|
||||
defer os.Remove(configStateFile)
|
||||
|
||||
errChan := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("server panic: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := server.Start(false); err != nil {
|
||||
errChan <- fmt.Errorf("server error: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||
}
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||
if !strings.HasPrefix(configPath, homeDir) {
|
||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Error starting quickshell: %v", err)
|
||||
}
|
||||
|
||||
// Write PID file for the quickshell child process
|
||||
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
||||
log.Warnf("Failed to write PID file: %v", err)
|
||||
}
|
||||
defer removePIDFile()
|
||||
|
||||
defer func() {
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
}()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
||||
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
||||
} else {
|
||||
errChan <- fmt.Errorf("quickshell exited")
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigChan:
|
||||
// Handle SIGUSR1 restart for non-session managed processes
|
||||
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||
execDetachedRestart(os.Getpid())
|
||||
// Exit immediately to avoid race conditions with detached restart
|
||||
return
|
||||
}
|
||||
|
||||
// All other signals: clean shutdown
|
||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||
cancel()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
os.Remove(socketPath)
|
||||
return
|
||||
|
||||
case err := <-errChan:
|
||||
log.Error(err)
|
||||
cancel()
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
os.Remove(socketPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restartShell() {
|
||||
pids := getAllDMSPIDs()
|
||||
|
||||
if len(pids) == 0 {
|
||||
log.Info("No running DMS shell instances found. Starting daemon...")
|
||||
runShellDaemon(false)
|
||||
return
|
||||
}
|
||||
|
||||
currentPid := os.Getpid()
|
||||
uniquePids := make(map[int]bool)
|
||||
|
||||
for _, pid := range pids {
|
||||
if pid != currentPid {
|
||||
uniquePids[pid] = true
|
||||
}
|
||||
}
|
||||
|
||||
for pid := range uniquePids {
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
log.Errorf("Error finding process %d: %v", pid, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := proc.Signal(syscall.SIGUSR1); err != nil {
|
||||
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
|
||||
} else {
|
||||
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func killShell() {
|
||||
// Get all tracked DMS PIDs from PID files
|
||||
pids := getAllDMSPIDs()
|
||||
|
||||
if len(pids) == 0 {
|
||||
log.Info("No running DMS shell instances found.")
|
||||
return
|
||||
}
|
||||
|
||||
currentPid := os.Getpid()
|
||||
uniquePids := make(map[int]bool)
|
||||
|
||||
// Deduplicate and filter out current process
|
||||
for _, pid := range pids {
|
||||
if pid != currentPid {
|
||||
uniquePids[pid] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Kill all tracked processes
|
||||
for pid := range uniquePids {
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
log.Errorf("Error finding process %d: %v", pid, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if process is still alive before killing
|
||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := proc.Kill(); err != nil {
|
||||
log.Errorf("Error killing process %d: %v", pid, err)
|
||||
} else {
|
||||
log.Infof("Killed DMS process with PID %d", pid)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any remaining PID files
|
||||
dir := getRuntimeDir()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".pid") {
|
||||
pidFile := filepath.Join(dir, entry.Name())
|
||||
os.Remove(pidFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runShellDaemon(session bool) {
|
||||
isSessionManaged = session
|
||||
// Check if this is the daemon child process by looking for the hidden flag
|
||||
isDaemonChild := false
|
||||
for _, arg := range os.Args {
|
||||
if arg == "--daemon-child" {
|
||||
isDaemonChild = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isDaemonChild {
|
||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||
|
||||
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Error starting daemon: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
socketPath := server.GetSocketPath()
|
||||
|
||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
|
||||
log.Warnf("Failed to write config state file: %v", err)
|
||||
}
|
||||
defer os.Remove(configStateFile)
|
||||
|
||||
errChan := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("server panic: %v", r)
|
||||
}
|
||||
}()
|
||||
server.CLIVersion = Version
|
||||
if err := server.Start(false); err != nil {
|
||||
errChan <- fmt.Errorf("server error: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||
}
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||
if !strings.HasPrefix(configPath, homeDir) {
|
||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
||||
}
|
||||
}
|
||||
|
||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening /dev/null: %v", err)
|
||||
}
|
||||
defer devNull.Close()
|
||||
|
||||
cmd.Stdin = devNull
|
||||
cmd.Stdout = devNull
|
||||
cmd.Stderr = devNull
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Error starting daemon: %v", err)
|
||||
}
|
||||
|
||||
// Write PID file for the quickshell child process
|
||||
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
||||
log.Warnf("Failed to write PID file: %v", err)
|
||||
}
|
||||
defer removePIDFile()
|
||||
|
||||
defer func() {
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
}()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
||||
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
||||
} else {
|
||||
errChan <- fmt.Errorf("quickshell exited")
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigChan:
|
||||
// Handle SIGUSR1 restart for non-session managed processes
|
||||
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||
execDetachedRestart(os.Getpid())
|
||||
// Exit immediately to avoid race conditions with detached restart
|
||||
return
|
||||
}
|
||||
|
||||
// All other signals: clean shutdown
|
||||
cancel()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
os.Remove(socketPath)
|
||||
return
|
||||
|
||||
case <-errChan:
|
||||
cancel()
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
os.Remove(socketPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runShellIPCCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
log.Error("IPC command requires arguments")
|
||||
log.Info("Usage: dms ipc <command> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if args[0] != "call" {
|
||||
args = append([]string{"call"}, args...)
|
||||
}
|
||||
|
||||
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("Error running IPC command: %v", err)
|
||||
}
|
||||
}
|
||||
53
core/cmd/dms/ui.go
Normal file
53
core/cmd/dms/ui.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func printASCII() {
|
||||
fmt.Print(getThemedASCII())
|
||||
}
|
||||
|
||||
func getThemedASCII() string {
|
||||
theme := tui.TerminalTheme()
|
||||
|
||||
logo := `
|
||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
||||
|
||||
style := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true)
|
||||
|
||||
return style.Render(logo) + "\n"
|
||||
}
|
||||
|
||||
func getHelpTemplate() string {
|
||||
return getThemedASCII() + `
|
||||
{{.Long}}
|
||||
|
||||
Usage:
|
||||
{{.UseLine}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
||||
|
||||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
||||
{{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
||||
`
|
||||
}
|
||||
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
|
||||
}
|
||||
69
core/go.mod
Normal file
69
core/go.mod
Normal file
@@ -0,0 +1,69 @@
|
||||
module github.com/AvengeMedia/DankMaterialShell/core
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/godbus/dbus/v5 v5.2.0
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.2 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
167
core/go.sum
Normal file
167
core/go.sum
Normal file
@@ -0,0 +1,167 @@
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
|
||||
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
|
||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
|
||||
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
|
||||
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
86
core/install.sh
Executable file
86
core/install.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check for root privileges
|
||||
if [ "$(id -u)" == "0" ]; then
|
||||
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if running on Linux
|
||||
if [ "$(uname)" != "Linux" ]; then
|
||||
printf "%bError: This installer only supports Linux systems%b\n" "$RED" "$NC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
ARCH="amd64"
|
||||
;;
|
||||
aarch64)
|
||||
ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
||||
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Get the latest release version
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
printf "%bError: Could not fetch latest version%b\n" "$RED" "$NC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "%bInstalling Dankinstall %s for %s...%b\n" "$GREEN" "$LATEST_VERSION" "$ARCH" "$NC"
|
||||
|
||||
# Download and install
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd "$TEMP_DIR" || exit 1
|
||||
|
||||
# Download the gzipped binary and its checksum
|
||||
printf "%bDownloading installer...%b\n" "$GREEN" "$NC"
|
||||
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz"
|
||||
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
||||
|
||||
# Get the expected checksum
|
||||
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
|
||||
|
||||
# Calculate actual checksum
|
||||
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
||||
ACTUAL_CHECKSUM=$(sha256sum installer.gz | awk '{print $1}')
|
||||
|
||||
# Compare checksums
|
||||
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
|
||||
printf "%bError: Checksum verification failed%b\n" "$RED" "$NC"
|
||||
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
||||
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
||||
printf "The downloaded file may be corrupted or tampered with\n"
|
||||
cd - > /dev/null
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Decompress the binary
|
||||
printf "%bDecompressing installer...%b\n" "$GREEN" "$NC"
|
||||
gunzip installer.gz
|
||||
chmod +x installer
|
||||
|
||||
# Execute the installer
|
||||
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
||||
./installer
|
||||
|
||||
# Cleanup
|
||||
cd - > /dev/null
|
||||
rm -rf "$TEMP_DIR"
|
||||
574
core/internal/config/deployer.go
Normal file
574
core/internal/config/deployer.go
Normal file
@@ -0,0 +1,574 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
)
|
||||
|
||||
type ConfigDeployer struct {
|
||||
logChan chan<- string
|
||||
}
|
||||
|
||||
type DeploymentResult struct {
|
||||
ConfigType string
|
||||
Path string
|
||||
BackupPath string
|
||||
Deployed bool
|
||||
Error error
|
||||
}
|
||||
|
||||
func NewConfigDeployer(logChan chan<- string) *ConfigDeployer {
|
||||
return &ConfigDeployer{
|
||||
logChan: logChan,
|
||||
}
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) log(message string) {
|
||||
if cd.logChan != nil {
|
||||
cd.logChan <- message
|
||||
}
|
||||
}
|
||||
|
||||
// DeployConfigurations deploys all necessary configurations based on the chosen window manager
|
||||
func (cd *ConfigDeployer) DeployConfigurations(ctx context.Context, wm deps.WindowManager) ([]DeploymentResult, error) {
|
||||
return cd.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
// DeployConfigurationsWithTerminal deploys all necessary configurations based on chosen window manager and terminal
|
||||
func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]DeploymentResult, error) {
|
||||
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
|
||||
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
shouldReplaceConfig := func(configType string) bool {
|
||||
if replaceConfigs == nil {
|
||||
return true
|
||||
}
|
||||
replace, exists := replaceConfigs[configType]
|
||||
return !exists || replace
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
if shouldReplaceConfig("Niri") {
|
||||
result, err := cd.deployNiriConfig(terminal)
|
||||
results = append(results, result)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.WindowManagerHyprland:
|
||||
if shouldReplaceConfig("Hyprland") {
|
||||
result, err := cd.deployHyprlandConfig(terminal)
|
||||
results = append(results, result)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
if shouldReplaceConfig("Ghostty") {
|
||||
ghosttyResults, err := cd.deployGhosttyConfig()
|
||||
results = append(results, ghosttyResults...)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Ghostty config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.TerminalKitty:
|
||||
if shouldReplaceConfig("Kitty") {
|
||||
kittyResults, err := cd.deployKittyConfig()
|
||||
results = append(results, kittyResults...)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Kitty config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.TerminalAlacritty:
|
||||
if shouldReplaceConfig("Alacritty") {
|
||||
alacrittyResults, err := cd.deployAlacrittyConfig()
|
||||
results = append(results, alacrittyResults...)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Alacritty config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// deployNiriConfig handles Niri configuration deployment with backup and merging
|
||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
ConfigType: "Niri",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(result.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
var existingConfig string
|
||||
if _, err := os.Stat(result.Path); err == nil {
|
||||
cd.log("Found existing Niri configuration")
|
||||
|
||||
existingData, err := os.ReadFile(result.Path)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
existingConfig = string(existingData)
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
result.BackupPath = result.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||
}
|
||||
|
||||
// Detect polkit agent path
|
||||
polkitPath, err := cd.detectPolkitAgent()
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
||||
}
|
||||
|
||||
// Determine terminal command based on choice
|
||||
var terminalCommand string
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
terminalCommand = "ghostty"
|
||||
case deps.TerminalKitty:
|
||||
terminalCommand = "kitty"
|
||||
case deps.TerminalAlacritty:
|
||||
terminalCommand = "alacritty"
|
||||
default:
|
||||
terminalCommand = "ghostty" // fallback to ghostty
|
||||
}
|
||||
|
||||
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
|
||||
// If there was an existing config, merge the output sections
|
||||
if existingConfig != "" {
|
||||
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
||||
} else {
|
||||
newConfig = mergedConfig
|
||||
cd.log("Successfully merged existing output sections")
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.Deployed = true
|
||||
cd.log("Successfully deployed Niri configuration")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
mainResult := DeploymentResult{
|
||||
ConfigType: "Ghostty",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(mainResult.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||
cd.log("Found existing Ghostty configuration")
|
||||
|
||||
existingData, err := os.ReadFile(mainResult.Path)
|
||||
if err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
mainResult.Deployed = true
|
||||
cd.log("Successfully deployed Ghostty configuration")
|
||||
results = append(results, mainResult)
|
||||
|
||||
colorResult := DeploymentResult{
|
||||
ConfigType: "Ghostty Colors",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
||||
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
||||
return results, colorResult.Error
|
||||
}
|
||||
|
||||
colorResult.Deployed = true
|
||||
cd.log("Successfully deployed Ghostty color configuration")
|
||||
results = append(results, colorResult)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
mainResult := DeploymentResult{
|
||||
ConfigType: "Kitty",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(mainResult.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||
cd.log("Found existing Kitty configuration")
|
||||
|
||||
existingData, err := os.ReadFile(mainResult.Path)
|
||||
if err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
mainResult.Deployed = true
|
||||
cd.log("Successfully deployed Kitty configuration")
|
||||
results = append(results, mainResult)
|
||||
|
||||
themeResult := DeploymentResult{
|
||||
ConfigType: "Kitty Theme",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
|
||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||
return results, themeResult.Error
|
||||
}
|
||||
|
||||
themeResult.Deployed = true
|
||||
cd.log("Successfully deployed Kitty theme configuration")
|
||||
results = append(results, themeResult)
|
||||
|
||||
tabsResult := DeploymentResult{
|
||||
ConfigType: "Kitty Tabs",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
|
||||
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
||||
return results, tabsResult.Error
|
||||
}
|
||||
|
||||
tabsResult.Deployed = true
|
||||
cd.log("Successfully deployed Kitty tabs configuration")
|
||||
results = append(results, tabsResult)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
mainResult := DeploymentResult{
|
||||
ConfigType: "Alacritty",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(mainResult.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||
cd.log("Found existing Alacritty configuration")
|
||||
|
||||
existingData, err := os.ReadFile(mainResult.Path)
|
||||
if err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
mainResult.Deployed = true
|
||||
cd.log("Successfully deployed Alacritty configuration")
|
||||
results = append(results, mainResult)
|
||||
|
||||
themeResult := DeploymentResult{
|
||||
ConfigType: "Alacritty Theme",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
|
||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||
return results, themeResult.Error
|
||||
}
|
||||
|
||||
themeResult.Deployed = true
|
||||
cd.log("Successfully deployed Alacritty theme configuration")
|
||||
results = append(results, themeResult)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// detectPolkitAgent tries to find the polkit authentication agent on the system
|
||||
// Prioritizes mate-polkit paths since that's what we install
|
||||
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
|
||||
// Prioritize mate-polkit paths first
|
||||
matePaths := []string{
|
||||
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
|
||||
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
|
||||
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
|
||||
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
|
||||
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
|
||||
}
|
||||
|
||||
for _, path := range matePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to other polkit agents if mate-polkit is not found
|
||||
fallbackPaths := []string{
|
||||
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
||||
"/usr/libexec/polkit-gnome-authentication-agent-1",
|
||||
}
|
||||
|
||||
for _, path := range fallbackPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no polkit agent found in common locations")
|
||||
}
|
||||
|
||||
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
||||
// Regular expression to match output sections (including commented ones)
|
||||
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
|
||||
// Find all output sections in the existing config
|
||||
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingOutputs) == 0 {
|
||||
// No output sections to merge
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
// Remove the example output section from the new config
|
||||
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
||||
|
||||
// Find where to insert the output sections (after the input section)
|
||||
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
||||
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
||||
|
||||
if len(inputMatches) < 1 {
|
||||
return "", fmt.Errorf("could not find insertion point for output sections")
|
||||
}
|
||||
|
||||
// Insert after the first closing brace (end of input section)
|
||||
insertPos := inputMatches[0][1]
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(mergedConfig[:insertPos])
|
||||
builder.WriteString("\n// Outputs from existing configuration\n")
|
||||
|
||||
for _, output := range existingOutputs {
|
||||
builder.WriteString(output)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
builder.WriteString(mergedConfig[insertPos:])
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
ConfigType: "Hyprland",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(result.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
var existingConfig string
|
||||
if _, err := os.Stat(result.Path); err == nil {
|
||||
cd.log("Found existing Hyprland configuration")
|
||||
|
||||
existingData, err := os.ReadFile(result.Path)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
existingConfig = string(existingData)
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
result.BackupPath = result.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||
}
|
||||
|
||||
// Detect polkit agent path
|
||||
polkitPath, err := cd.detectPolkitAgent()
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
||||
}
|
||||
|
||||
// Determine terminal command based on choice
|
||||
var terminalCommand string
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
terminalCommand = "ghostty"
|
||||
case deps.TerminalKitty:
|
||||
terminalCommand = "kitty"
|
||||
case deps.TerminalAlacritty:
|
||||
terminalCommand = "alacritty"
|
||||
default:
|
||||
terminalCommand = "ghostty" // fallback to ghostty
|
||||
}
|
||||
|
||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
|
||||
// If there was an existing config, merge the monitor sections
|
||||
if existingConfig != "" {
|
||||
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
||||
} else {
|
||||
newConfig = mergedConfig
|
||||
cd.log("Successfully merged existing monitor sections")
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.Deployed = true
|
||||
cd.log("Successfully deployed Hyprland configuration")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
||||
// Regular expression to match monitor lines (including commented ones)
|
||||
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
|
||||
// Also matches commented versions: # monitor = ...
|
||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||
|
||||
// Find all monitor lines in the existing config
|
||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingMonitors) == 0 {
|
||||
// No monitor sections to merge
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
// Remove the example monitor line from the new config
|
||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||
|
||||
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
|
||||
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
||||
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
||||
|
||||
if headerMatch == nil {
|
||||
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
||||
}
|
||||
|
||||
// Insert after the header
|
||||
insertPos := headerMatch[1] + 1 // +1 for the newline
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(mergedConfig[:insertPos])
|
||||
builder.WriteString("# Monitors from existing configuration\n")
|
||||
|
||||
for _, monitor := range existingMonitors {
|
||||
builder.WriteString(monitor)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
builder.WriteString(mergedConfig[insertPos:])
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
660
core/internal/config/deployer_test.go
Normal file
660
core/internal/config/deployer_test.go
Normal file
@@ -0,0 +1,660 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDetectPolkitAgent(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
// This test depends on the system having a polkit agent installed
|
||||
// We'll just test that the function doesn't crash and returns some path or error
|
||||
path, err := cd.detectPolkitAgent()
|
||||
|
||||
if err != nil {
|
||||
// If no polkit agent is found, that's okay for testing
|
||||
assert.Contains(t, err.Error(), "no polkit agent found")
|
||||
} else {
|
||||
// If found, it should be a valid path
|
||||
assert.NotEmpty(t, path)
|
||||
assert.True(t, strings.Contains(path, "polkit"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNiriOutputSections(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newConfig string
|
||||
existingConfig string
|
||||
wantError bool
|
||||
wantContains []string
|
||||
}{
|
||||
{
|
||||
name: "no existing outputs",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{"gaps 5"}, // Should keep new config
|
||||
},
|
||||
{
|
||||
name: "merge single output",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
output "eDP-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=0 y=0
|
||||
scale 1.0
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"gaps 5", // New config preserved
|
||||
`output "eDP-1"`, // Existing output merged
|
||||
"1920x1080@60.000000", // Existing output details
|
||||
"Outputs from existing configuration", // Comment added
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge multiple outputs",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
output "eDP-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=0 y=0
|
||||
scale 1.0
|
||||
}
|
||||
/-output "HDMI-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=1920 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"gaps 5", // New config preserved
|
||||
`output "eDP-1"`, // First existing output
|
||||
`/-output "HDMI-1"`, // Second existing output (commented)
|
||||
"1920x1080@60.000000", // Output details
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge commented outputs",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=0 y=0
|
||||
scale 1.0
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"gaps 5", // New config preserved
|
||||
`/-output "eDP-1"`, // Commented output preserved
|
||||
"1920x1080@60.000000", // Output details
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||
}
|
||||
|
||||
assert.NotContains(t, result, `/-output "eDP-2"`, "example output should be removed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDeploymentFlow(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy ghostty config to empty directory", func(t *testing.T) {
|
||||
results, err := cd.deployGhosttyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Ghostty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.Empty(t, mainResult.BackupPath)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
|
||||
content, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "window-decoration = false")
|
||||
|
||||
colorResult := results[1]
|
||||
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
|
||||
assert.True(t, colorResult.Deployed)
|
||||
assert.FileExists(t, colorResult.Path)
|
||||
|
||||
colorContent, err := os.ReadFile(colorResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(colorContent), "background = #101418")
|
||||
})
|
||||
|
||||
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
||||
existingContent := "# Old config\nfont-size = 14\n"
|
||||
ghosttyPath := getGhosttyPath()
|
||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := cd.deployGhosttyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Ghostty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.NotEmpty(t, mainResult.BackupPath)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
assert.FileExists(t, mainResult.BackupPath)
|
||||
|
||||
backupContent, err := os.ReadFile(mainResult.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
|
||||
newContent, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(newContent), "# Old config")
|
||||
|
||||
colorResult := results[1]
|
||||
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
|
||||
assert.True(t, colorResult.Deployed)
|
||||
assert.FileExists(t, colorResult.Path)
|
||||
})
|
||||
}
|
||||
|
||||
func getGhosttyPath() string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
||||
}
|
||||
|
||||
func TestPolkitPathInjection(t *testing.T) {
|
||||
|
||||
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
||||
other content`
|
||||
|
||||
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
|
||||
|
||||
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
|
||||
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
|
||||
}
|
||||
|
||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newConfig string
|
||||
existingConfig string
|
||||
wantError bool
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "no existing monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================
|
||||
env = XDG_CURRENT_DESKTOP,niri`,
|
||||
existingConfig: `# Some other config
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
|
||||
},
|
||||
{
|
||||
name: "merge single monitor",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================`,
|
||||
existingConfig: `# My config
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"MONITOR CONFIG",
|
||||
"monitor = DP-1, 1920x1080@144, 0x0, 1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge multiple monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================`,
|
||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"monitor = DP-1",
|
||||
"# monitor = HDMI-A-1", // Commented monitor preserved
|
||||
"monitor = eDP-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preserve commented monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================`,
|
||||
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"# monitor = DP-1",
|
||||
"# monitor = HDMI-A-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no monitor config section",
|
||||
newConfig: `# Some config without monitor section
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||
}
|
||||
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||
assert.True(t, result.Deployed)
|
||||
assert.Empty(t, result.BackupPath)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
content, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
||||
assert.Contains(t, string(content), "exec-once = ")
|
||||
})
|
||||
|
||||
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
||||
existingContent := `# My existing Hyprland config
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||
|
||||
general {
|
||||
gaps_in = 10
|
||||
}
|
||||
`
|
||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||
assert.True(t, result.Deployed)
|
||||
assert.NotEmpty(t, result.BackupPath)
|
||||
assert.FileExists(t, result.Path)
|
||||
assert.FileExists(t, result.BackupPath)
|
||||
|
||||
backupContent, err := os.ReadFile(result.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
|
||||
newContent, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNiriConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, NiriConfig, "input {")
|
||||
assert.Contains(t, NiriConfig, "layout {")
|
||||
assert.Contains(t, NiriConfig, "binds {")
|
||||
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
||||
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||
}
|
||||
|
||||
func TestHyprlandConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
|
||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
||||
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
|
||||
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
|
||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
|
||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
||||
}
|
||||
|
||||
func TestGhosttyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
|
||||
}
|
||||
|
||||
func TestGhosttyColorConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, GhosttyColorConfig, "background = #101418")
|
||||
assert.Contains(t, GhosttyColorConfig, "foreground = #e0e2e8")
|
||||
assert.Contains(t, GhosttyColorConfig, "cursor-color = #9dcbfb")
|
||||
assert.Contains(t, GhosttyColorConfig, "palette = 0=#101418")
|
||||
assert.Contains(t, GhosttyColorConfig, "palette = 15=#ffffff")
|
||||
}
|
||||
|
||||
func TestKittyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyConfig, "font_size 12.0")
|
||||
assert.Contains(t, KittyConfig, "window_padding_width 12")
|
||||
assert.Contains(t, KittyConfig, "background_opacity 1.0")
|
||||
assert.Contains(t, KittyConfig, "include dank-tabs.conf")
|
||||
assert.Contains(t, KittyConfig, "include dank-theme.conf")
|
||||
}
|
||||
|
||||
func TestKittyThemeConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
|
||||
assert.Contains(t, KittyThemeConfig, "background #101418")
|
||||
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
|
||||
assert.Contains(t, KittyThemeConfig, "color0 #101418")
|
||||
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
|
||||
}
|
||||
|
||||
func TestKittyTabsConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyTabsConfig, "tab_bar_style powerline")
|
||||
assert.Contains(t, KittyTabsConfig, "tab_powerline_style slanted")
|
||||
assert.Contains(t, KittyTabsConfig, "active_tab_background #124a73")
|
||||
assert.Contains(t, KittyTabsConfig, "inactive_tab_background #101418")
|
||||
}
|
||||
|
||||
func TestAlacrittyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, AlacrittyConfig, "[general]")
|
||||
assert.Contains(t, AlacrittyConfig, "~/.config/alacritty/dank-theme.toml")
|
||||
assert.Contains(t, AlacrittyConfig, "[window]")
|
||||
assert.Contains(t, AlacrittyConfig, "decorations = \"None\"")
|
||||
assert.Contains(t, AlacrittyConfig, "padding = { x = 12, y = 12 }")
|
||||
assert.Contains(t, AlacrittyConfig, "[cursor]")
|
||||
assert.Contains(t, AlacrittyConfig, "[keyboard]")
|
||||
}
|
||||
|
||||
func TestAlacrittyThemeConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.primary]")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "background = '#101418'")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "foreground = '#e0e2e8'")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.cursor]")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "cursor = '#9dcbfb'")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.normal]")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.bright]")
|
||||
}
|
||||
|
||||
func TestKittyConfigDeployment(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-kitty-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy kitty config to empty directory", func(t *testing.T) {
|
||||
results, err := cd.deployKittyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 3)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Kitty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
|
||||
content, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "include dank-theme.conf")
|
||||
|
||||
themeResult := results[1]
|
||||
assert.Equal(t, "Kitty Theme", themeResult.ConfigType)
|
||||
assert.True(t, themeResult.Deployed)
|
||||
assert.FileExists(t, themeResult.Path)
|
||||
|
||||
tabsResult := results[2]
|
||||
assert.Equal(t, "Kitty Tabs", tabsResult.ConfigType)
|
||||
assert.True(t, tabsResult.Deployed)
|
||||
assert.FileExists(t, tabsResult.Path)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlacrittyConfigDeployment(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-alacritty-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy alacritty config to empty directory", func(t *testing.T) {
|
||||
results, err := cd.deployAlacrittyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Alacritty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
|
||||
content, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "~/.config/alacritty/dank-theme.toml")
|
||||
assert.Contains(t, string(content), "[window]")
|
||||
|
||||
themeResult := results[1]
|
||||
assert.Equal(t, "Alacritty Theme", themeResult.ConfigType)
|
||||
assert.True(t, themeResult.Deployed)
|
||||
assert.FileExists(t, themeResult.Path)
|
||||
|
||||
themeContent, err := os.ReadFile(themeResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(themeContent), "[colors.primary]")
|
||||
assert.Contains(t, string(themeContent), "background = '#101418'")
|
||||
})
|
||||
|
||||
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
||||
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
||||
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := cd.deployAlacrittyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.NotEmpty(t, mainResult.BackupPath)
|
||||
assert.FileExists(t, mainResult.BackupPath)
|
||||
|
||||
backupContent, err := os.ReadFile(mainResult.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
|
||||
newContent, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(newContent), "# Old alacritty config")
|
||||
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
||||
})
|
||||
}
|
||||
53
core/internal/config/dms.go
Normal file
53
core/internal/config/dms.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
|
||||
func LocateDMSConfig() (string, error) {
|
||||
var primaryPaths []string
|
||||
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
configHome = filepath.Join(homeDir, ".config")
|
||||
}
|
||||
}
|
||||
|
||||
if configHome != "" {
|
||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||
}
|
||||
|
||||
primaryPaths = append(primaryPaths, "/usr/share/quickshell/dms")
|
||||
|
||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||
if configDirs == "" {
|
||||
configDirs = "/etc/xdg"
|
||||
}
|
||||
|
||||
for _, dir := range strings.Split(configDirs, ":") {
|
||||
if dir != "" {
|
||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||
}
|
||||
}
|
||||
|
||||
// Build search paths with secondary (monorepo) paths interleaved
|
||||
var searchPaths []string
|
||||
for _, path := range primaryPaths {
|
||||
searchPaths = append(searchPaths, path)
|
||||
searchPaths = append(searchPaths, filepath.Join(path, "quickshell"))
|
||||
}
|
||||
|
||||
for _, path := range searchPaths {
|
||||
shellPath := filepath.Join(path, "shell.qml")
|
||||
if info, err := os.Stat(shellPath); err == nil && !info.IsDir() {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find DMS config (shell.qml) in any valid config path")
|
||||
}
|
||||
31
core/internal/config/embedded/alacritty-theme.toml
Normal file
31
core/internal/config/embedded/alacritty-theme.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[colors.primary]
|
||||
background = '#101418'
|
||||
foreground = '#e0e2e8'
|
||||
|
||||
[colors.selection]
|
||||
text = '#e0e2e8'
|
||||
background = '#124a73'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#101418'
|
||||
cursor = '#9dcbfb'
|
||||
|
||||
[colors.normal]
|
||||
black = '#101418'
|
||||
red = '#d75a59'
|
||||
green = '#8ed88c'
|
||||
yellow = '#e0d99d'
|
||||
blue = '#4087bc'
|
||||
magenta = '#839fbc'
|
||||
cyan = '#9dcbfb'
|
||||
white = '#abb2bf'
|
||||
|
||||
[colors.bright]
|
||||
black = '#5c6370'
|
||||
red = '#e57e7e'
|
||||
green = '#a2e5a0'
|
||||
yellow = '#efe9b3'
|
||||
blue = '#a7d9ff'
|
||||
magenta = '#3d8197'
|
||||
cyan = '#5c7ba3'
|
||||
white = '#ffffff'
|
||||
37
core/internal/config/embedded/alacritty.toml
Normal file
37
core/internal/config/embedded/alacritty.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[general]
|
||||
import = [
|
||||
"~/.config/alacritty/dank-theme.toml"
|
||||
]
|
||||
|
||||
[window]
|
||||
decorations = "None"
|
||||
padding = { x = 12, y = 12 }
|
||||
opacity = 1.0
|
||||
|
||||
[scrolling]
|
||||
history = 3023
|
||||
|
||||
[cursor]
|
||||
style = { shape = "Block", blinking = "On" }
|
||||
blink_interval = 500
|
||||
unfocused_hollow = true
|
||||
|
||||
[mouse]
|
||||
hide_when_typing = true
|
||||
|
||||
[selection]
|
||||
save_to_clipboard = false
|
||||
|
||||
[bell]
|
||||
duration = 0
|
||||
|
||||
[keyboard]
|
||||
bindings = [
|
||||
{ key = "C", mods = "Control|Shift", action = "Copy" },
|
||||
{ key = "V", mods = "Control|Shift", action = "Paste" },
|
||||
{ key = "N", mods = "Control|Shift", action = "SpawnNewInstance" },
|
||||
{ key = "Equals", mods = "Control|Shift", action = "IncreaseFontSize" },
|
||||
{ key = "Minus", mods = "Control", action = "DecreaseFontSize" },
|
||||
{ key = "Key0", mods = "Control", action = "ResetFontSize" },
|
||||
{ key = "Enter", mods = "Shift", chars = "\n" },
|
||||
]
|
||||
21
core/internal/config/embedded/ghostty-colors.conf
Normal file
21
core/internal/config/embedded/ghostty-colors.conf
Normal file
@@ -0,0 +1,21 @@
|
||||
background = #101418
|
||||
foreground = #e0e2e8
|
||||
cursor-color = #9dcbfb
|
||||
selection-background = #124a73
|
||||
selection-foreground = #e0e2e8
|
||||
palette = 0=#101418
|
||||
palette = 1=#d75a59
|
||||
palette = 2=#8ed88c
|
||||
palette = 3=#e0d99d
|
||||
palette = 4=#4087bc
|
||||
palette = 5=#839fbc
|
||||
palette = 6=#9dcbfb
|
||||
palette = 7=#abb2bf
|
||||
palette = 8=#5c6370
|
||||
palette = 9=#e57e7e
|
||||
palette = 10=#a2e5a0
|
||||
palette = 11=#efe9b3
|
||||
palette = 12=#a7d9ff
|
||||
palette = 13=#3d8197
|
||||
palette = 14=#5c7ba3
|
||||
palette = 15=#ffffff
|
||||
51
core/internal/config/embedded/ghostty.conf
Normal file
51
core/internal/config/embedded/ghostty.conf
Normal file
@@ -0,0 +1,51 @@
|
||||
# Font Configuration
|
||||
font-size = 12
|
||||
|
||||
# Window Configuration
|
||||
window-decoration = false
|
||||
window-padding-x = 12
|
||||
window-padding-y = 12
|
||||
background-opacity = 1.0
|
||||
background-blur-radius = 32
|
||||
|
||||
# Cursor Configuration
|
||||
cursor-style = block
|
||||
cursor-style-blink = true
|
||||
|
||||
# Scrollback
|
||||
scrollback-limit = 3023
|
||||
|
||||
# Terminal features
|
||||
mouse-hide-while-typing = true
|
||||
copy-on-select = false
|
||||
confirm-close-surface = false
|
||||
|
||||
# Disable annoying copied to clipboard
|
||||
app-notifications = no-clipboard-copy,no-config-reload
|
||||
|
||||
# Key bindings for common actions
|
||||
#keybind = ctrl+c=copy_to_clipboard
|
||||
#keybind = ctrl+v=paste_from_clipboard
|
||||
keybind = ctrl+shift+n=new_window
|
||||
keybind = ctrl+t=new_tab
|
||||
keybind = ctrl+plus=increase_font_size:1
|
||||
keybind = ctrl+minus=decrease_font_size:1
|
||||
keybind = ctrl+zero=reset_font_size
|
||||
|
||||
# Material 3 UI elements
|
||||
unfocused-split-opacity = 0.7
|
||||
unfocused-split-fill = #44464f
|
||||
|
||||
# Tab configuration
|
||||
gtk-titlebar = false
|
||||
|
||||
# Shell integration
|
||||
shell-integration = detect
|
||||
shell-integration-features = cursor,sudo,title,no-cursor
|
||||
keybind = shift+enter=text:\n
|
||||
|
||||
# Rando stuff
|
||||
gtk-single-instance = true
|
||||
|
||||
# Dank color generation
|
||||
config-file = ./config-dankcolors
|
||||
292
core/internal/config/embedded/hyprland.conf
Normal file
292
core/internal/config/embedded/hyprland.conf
Normal file
@@ -0,0 +1,292 @@
|
||||
# Hyprland Configuration
|
||||
# https://wiki.hypr.land/Configuring/
|
||||
|
||||
# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
monitor = , preferred,auto,auto
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================
|
||||
env = QT_QPA_PLATFORM,wayland
|
||||
env = ELECTRON_OZONE_PLATFORM_HINT,auto
|
||||
env = QT_QPA_PLATFORMTHEME,gtk3
|
||||
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
|
||||
env = TERMINAL,{{TERMINAL_COMMAND}}
|
||||
|
||||
# ==================
|
||||
# STARTUP APPS
|
||||
# ==================
|
||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||
exec-once = dms run
|
||||
exec-once = {{POLKIT_AGENT_PATH}}
|
||||
|
||||
# ==================
|
||||
# INPUT CONFIG
|
||||
# ==================
|
||||
input {
|
||||
kb_layout = us
|
||||
numlock_by_default = true
|
||||
}
|
||||
|
||||
# ==================
|
||||
# GENERAL LAYOUT
|
||||
# ==================
|
||||
general {
|
||||
gaps_in = 5
|
||||
gaps_out = 5
|
||||
border_size = 0 # off in niri
|
||||
|
||||
col.active_border = rgba(707070ff)
|
||||
col.inactive_border = rgba(d0d0d0ff)
|
||||
|
||||
layout = dwindle
|
||||
}
|
||||
|
||||
# ==================
|
||||
# DECORATION
|
||||
# ==================
|
||||
decoration {
|
||||
rounding = 12
|
||||
|
||||
active_opacity = 1.0
|
||||
inactive_opacity = 0.9
|
||||
|
||||
shadow {
|
||||
enabled = true
|
||||
range = 30
|
||||
render_power = 5
|
||||
offset = 0 5
|
||||
color = rgba(00000070)
|
||||
}
|
||||
}
|
||||
|
||||
# ==================
|
||||
# ANIMATIONS
|
||||
# ==================
|
||||
animations {
|
||||
enabled = true
|
||||
|
||||
animation = windowsIn, 1, 3, default
|
||||
animation = windowsOut, 1, 3, default
|
||||
animation = workspaces, 1, 5, default
|
||||
animation = windowsMove, 1, 4, default
|
||||
animation = fade, 1, 3, default
|
||||
animation = border, 1, 3, default
|
||||
}
|
||||
|
||||
# ==================
|
||||
# LAYOUTS
|
||||
# ==================
|
||||
dwindle {
|
||||
preserve_split = true
|
||||
}
|
||||
|
||||
master {
|
||||
mfact = 0.5
|
||||
}
|
||||
|
||||
# ==================
|
||||
# MISC
|
||||
# ==================
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
disable_splash_rendering = true
|
||||
vrr = 1
|
||||
}
|
||||
|
||||
# ==================
|
||||
# WINDOW RULES
|
||||
# ==================
|
||||
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
|
||||
|
||||
windowrulev2 = rounding 12, class:^(org\.gnome\.)
|
||||
windowrulev2 = noborder, class:^(org\.gnome\.)
|
||||
|
||||
windowrulev2 = tile, class:^(gnome-control-center)$
|
||||
windowrulev2 = tile, class:^(pavucontrol)$
|
||||
windowrulev2 = tile, class:^(nm-connection-editor)$
|
||||
|
||||
windowrulev2 = float, class:^(gnome-calculator)$
|
||||
windowrulev2 = float, class:^(galculator)$
|
||||
windowrulev2 = float, class:^(blueman-manager)$
|
||||
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
|
||||
windowrulev2 = float, class:^(steam)$
|
||||
windowrulev2 = float, class:^(xdg-desktop-portal)$
|
||||
|
||||
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
|
||||
windowrulev2 = noborder, class:^(Alacritty)$
|
||||
windowrulev2 = noborder, class:^(zen)$
|
||||
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
|
||||
windowrulev2 = noborder, class:^(kitty)$
|
||||
|
||||
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
||||
windowrulev2 = float, class:^(zoom)$
|
||||
|
||||
# DMS windows floating by default
|
||||
windowrulev2 = float, class:^(org.quickshell)$
|
||||
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
||||
|
||||
layerrule = noanim, ^(quickshell)$
|
||||
|
||||
# ==================
|
||||
# KEYBINDINGS
|
||||
# ==================
|
||||
$mod = SUPER
|
||||
|
||||
# === Application Launchers ===
|
||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
||||
bind = $mod, M, exec, dms ipc call processlist toggle
|
||||
bind = $mod, comma, exec, dms ipc call settings toggle
|
||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
||||
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
|
||||
|
||||
# === Cheat sheet
|
||||
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
||||
|
||||
# === Security ===
|
||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
||||
bind = $mod SHIFT, E, exit
|
||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
|
||||
|
||||
# === Audio Controls ===
|
||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
||||
|
||||
# === Brightness Controls ===
|
||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||
|
||||
# === Window Management ===
|
||||
bind = $mod, Q, killactive
|
||||
bind = $mod, F, fullscreen, 1
|
||||
bind = $mod SHIFT, F, fullscreen, 0
|
||||
bind = $mod SHIFT, T, togglefloating
|
||||
bind = $mod, W, togglegroup
|
||||
|
||||
# === Focus Navigation ===
|
||||
bind = $mod, left, movefocus, l
|
||||
bind = $mod, down, movefocus, d
|
||||
bind = $mod, up, movefocus, u
|
||||
bind = $mod, right, movefocus, r
|
||||
bind = $mod, H, movefocus, l
|
||||
bind = $mod, J, movefocus, d
|
||||
bind = $mod, K, movefocus, u
|
||||
bind = $mod, L, movefocus, r
|
||||
|
||||
# === Window Movement ===
|
||||
bind = $mod SHIFT, left, movewindow, l
|
||||
bind = $mod SHIFT, down, movewindow, d
|
||||
bind = $mod SHIFT, up, movewindow, u
|
||||
bind = $mod SHIFT, right, movewindow, r
|
||||
bind = $mod SHIFT, H, movewindow, l
|
||||
bind = $mod SHIFT, J, movewindow, d
|
||||
bind = $mod SHIFT, K, movewindow, u
|
||||
bind = $mod SHIFT, L, movewindow, r
|
||||
|
||||
# === Column Navigation ===
|
||||
bind = $mod, Home, focuswindow, first
|
||||
bind = $mod, End, focuswindow, last
|
||||
|
||||
# === Monitor Navigation ===
|
||||
bind = $mod CTRL, left, focusmonitor, l
|
||||
bind = $mod CTRL, right, focusmonitor, r
|
||||
bind = $mod CTRL, H, focusmonitor, l
|
||||
bind = $mod CTRL, J, focusmonitor, d
|
||||
bind = $mod CTRL, K, focusmonitor, u
|
||||
bind = $mod CTRL, L, focusmonitor, r
|
||||
|
||||
# === Move to Monitor ===
|
||||
bind = $mod SHIFT CTRL, left, movewindow, mon:l
|
||||
bind = $mod SHIFT CTRL, down, movewindow, mon:d
|
||||
bind = $mod SHIFT CTRL, up, movewindow, mon:u
|
||||
bind = $mod SHIFT CTRL, right, movewindow, mon:r
|
||||
bind = $mod SHIFT CTRL, H, movewindow, mon:l
|
||||
bind = $mod SHIFT CTRL, J, movewindow, mon:d
|
||||
bind = $mod SHIFT CTRL, K, movewindow, mon:u
|
||||
bind = $mod SHIFT CTRL, L, movewindow, mon:r
|
||||
|
||||
# === Workspace Navigation ===
|
||||
bind = $mod, Page_Down, workspace, e+1
|
||||
bind = $mod, Page_Up, workspace, e-1
|
||||
bind = $mod, U, workspace, e+1
|
||||
bind = $mod, I, workspace, e-1
|
||||
bind = $mod CTRL, down, movetoworkspace, e+1
|
||||
bind = $mod CTRL, up, movetoworkspace, e-1
|
||||
bind = $mod CTRL, U, movetoworkspace, e+1
|
||||
bind = $mod CTRL, I, movetoworkspace, e-1
|
||||
|
||||
# === Move Workspaces ===
|
||||
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
|
||||
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
|
||||
bind = $mod SHIFT, U, movetoworkspace, e+1
|
||||
bind = $mod SHIFT, I, movetoworkspace, e-1
|
||||
|
||||
# === Mouse Wheel Navigation ===
|
||||
bind = $mod, mouse_down, workspace, e+1
|
||||
bind = $mod, mouse_up, workspace, e-1
|
||||
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
|
||||
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
|
||||
|
||||
# === Numbered Workspaces ===
|
||||
bind = $mod, 1, workspace, 1
|
||||
bind = $mod, 2, workspace, 2
|
||||
bind = $mod, 3, workspace, 3
|
||||
bind = $mod, 4, workspace, 4
|
||||
bind = $mod, 5, workspace, 5
|
||||
bind = $mod, 6, workspace, 6
|
||||
bind = $mod, 7, workspace, 7
|
||||
bind = $mod, 8, workspace, 8
|
||||
bind = $mod, 9, workspace, 9
|
||||
|
||||
# === Move to Numbered Workspaces ===
|
||||
bind = $mod SHIFT, 1, movetoworkspace, 1
|
||||
bind = $mod SHIFT, 2, movetoworkspace, 2
|
||||
bind = $mod SHIFT, 3, movetoworkspace, 3
|
||||
bind = $mod SHIFT, 4, movetoworkspace, 4
|
||||
bind = $mod SHIFT, 5, movetoworkspace, 5
|
||||
bind = $mod SHIFT, 6, movetoworkspace, 6
|
||||
bind = $mod SHIFT, 7, movetoworkspace, 7
|
||||
bind = $mod SHIFT, 8, movetoworkspace, 8
|
||||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
||||
|
||||
# === Column Management ===
|
||||
bind = $mod, bracketleft, layoutmsg, preselect l
|
||||
bind = $mod, bracketright, layoutmsg, preselect r
|
||||
|
||||
# === Sizing & Layout ===
|
||||
bind = $mod, R, layoutmsg, togglesplit
|
||||
bind = $mod CTRL, F, resizeactive, exact 100%
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindmd = $mod, mouse:272, Move window, movewindow
|
||||
bindmd = $mod, mouse:273, Resize window, resizewindow
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
|
||||
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
|
||||
|
||||
# === Manual Sizing ===
|
||||
binde = $mod, minus, resizeactive, -10% 0
|
||||
binde = $mod, equal, resizeactive, 10% 0
|
||||
binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
||||
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
||||
|
||||
# === Screenshots ===
|
||||
bind = , XF86Launch1, exec, grimblast copy area
|
||||
bind = CTRL, XF86Launch1, exec, grimblast copy screen
|
||||
bind = ALT, XF86Launch1, exec, grimblast copy active
|
||||
bind = , Print, exec, grimblast copy area
|
||||
bind = CTRL, Print, exec, grimblast copy screen
|
||||
bind = ALT, Print, exec, grimblast copy active
|
||||
|
||||
# === System Controls ===
|
||||
bind = $mod SHIFT, P, dpms, off
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user