mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 02:52:07 -04:00
Compare commits
308 Commits
stable
...
293c2a0035
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293c2a0035 | ||
|
|
9a5fa50541 | ||
|
|
d5ceea8a56 | ||
|
|
faa5e7e02d | ||
|
|
516c478f3d | ||
|
|
906c6a2501 | ||
|
|
86d8fe4fa4 | ||
|
|
9b44bc3259 | ||
|
|
59b6d2237b | ||
|
|
7e559cc0bb | ||
|
|
fd1facfce8 | ||
|
|
8f26193cc3 | ||
|
|
43b2e5315d | ||
|
|
5cad89e9cc | ||
|
|
3804d2f00b | ||
|
|
4d649468d5 | ||
|
|
c5f145be36 | ||
|
|
76dff870a7 | ||
|
|
6c8d3fc007 | ||
|
|
e7ffa23016 | ||
|
|
4266c064a9 | ||
|
|
5f631b36cd | ||
|
|
be8326f497 | ||
|
|
07dbba6c53 | ||
|
|
a53b9afb44 | ||
|
|
a0c7ffd6b9 | ||
|
|
7ca1d2325a | ||
|
|
8d0f256f74 | ||
|
|
1a9449da1b | ||
|
|
1caf8942b7 | ||
|
|
9efbcbcd20 | ||
|
|
3d07b8c9c1 | ||
|
|
dae74a40c0 | ||
|
|
959190dcbc | ||
|
|
1e48976ae5 | ||
|
|
0a8c111e12 | ||
|
|
19c786c0be | ||
|
|
7f8b260560 | ||
|
|
368536f698 | ||
|
|
b227221df6 | ||
|
|
8e047f45f5 | ||
|
|
fbe8cbb23f | ||
|
|
28315a165f | ||
|
|
1b32829dac | ||
|
|
1fce29324f | ||
|
|
1fab90178a | ||
|
|
eb04ab7dca | ||
|
|
e9fa2c78ee | ||
|
|
59dae954cd | ||
|
|
5c4ce86da4 | ||
|
|
0cf2c40377 | ||
|
|
679a59ad76 | ||
|
|
db3209afbe | ||
|
|
f0be36062e | ||
|
|
9578d6daf9 | ||
|
|
cc6766135d | ||
|
|
28c9bb0925 | ||
|
|
7826d827dd | ||
|
|
7f392acc54 | ||
|
|
190fd662ad | ||
|
|
e18587c471 | ||
|
|
ddb079b62d | ||
|
|
e7c8d208e2 | ||
|
|
0e2162cf29 | ||
|
|
4cf9b0adc7 | ||
|
|
1661d32641 | ||
|
|
aa59187403 | ||
|
|
bb08e1233a | ||
|
|
5343e97ab2 | ||
|
|
edc544df7a | ||
|
|
a880edd9fb | ||
|
|
7e1d808d70 | ||
|
|
ce93f22669 | ||
|
|
a58037b968 | ||
|
|
ccf0b60935 | ||
|
|
aad7011b1c | ||
|
|
3bde7ef4d3 | ||
|
|
04555dbfa7 | ||
|
|
3b494aa591 | ||
|
|
365387c3cd | ||
|
|
bb74a0ca4d | ||
|
|
9cf2ef84b7 | ||
|
|
46aaf5ff77 | ||
|
|
c544bda5df | ||
|
|
e86227f05f | ||
|
|
53da60e4ca | ||
|
|
727d9c6c22 | ||
|
|
908e1f600e | ||
|
|
270d800df2 | ||
|
|
d445d182ea | ||
|
|
476256c9e7 | ||
|
|
06ea7373f7 | ||
|
|
e78ba77def | ||
|
|
7113afe9e2 | ||
|
|
1a2b6524e6 | ||
|
|
95c4aa9e4c | ||
|
|
9f2518c9e1 | ||
|
|
76c50a654a | ||
|
|
ded2c38551 | ||
|
|
772094eacd | ||
|
|
bddc2f6295 | ||
|
|
25dce2961b | ||
|
|
653cfbe6e0 | ||
|
|
c539311083 | ||
|
|
60118c5d5b | ||
|
|
c6b9b36566 | ||
|
|
fd5b1b7c00 | ||
|
|
ebc77b62c8 | ||
|
|
2ce888581f | ||
|
|
0e901b6404 | ||
|
|
688b9076e7 | ||
|
|
c6ec7579b6 | ||
|
|
9417edac8d | ||
|
|
6185cc79d7 | ||
|
|
4ecdba94c2 | ||
|
|
a11640d840 | ||
|
|
177a4c4095 | ||
|
|
63df19ab78 | ||
|
|
54e0eb5979 | ||
|
|
185284d422 | ||
|
|
ce240405d9 | ||
|
|
58b700ed0d | ||
|
|
d436fa4920 | ||
|
|
d58486193e | ||
|
|
e9404eb9b6 | ||
|
|
0fef4d515e | ||
|
|
86f9cf4376 | ||
|
|
acf63c57e8 | ||
|
|
baa956c3a1 | ||
|
|
bb2081a936 | ||
|
|
c984b0b9ae | ||
|
|
754bf8fa3c | ||
|
|
7840294517 | ||
|
|
caaee88654 | ||
|
|
e872ddc1e7 | ||
|
|
1eca9b4c2c | ||
|
|
fe5bd42e25 | ||
|
|
32d16d0673 | ||
|
|
27c26d35ab | ||
|
|
e04c919d78 | ||
|
|
246b6c44b0 | ||
|
|
847ddf7d38 | ||
|
|
16e8199f9e | ||
|
|
7d1519f546 | ||
|
|
1bf66ee482 | ||
|
|
39a43f4de5 | ||
|
|
971a511edb | ||
|
|
0f8e0bc2b4 | ||
|
|
537c44e354 | ||
|
|
db53a9a719 | ||
|
|
f4a10de790 | ||
|
|
8c9fe84d02 | ||
|
|
f0fcc77bdb | ||
|
|
cf4c4b7d69 | ||
|
|
7bb8499353 | ||
|
|
ee1a2bc7de | ||
|
|
20d383d4ab | ||
|
|
9cb0d8baf2 | ||
|
|
362ded3bc9 | ||
|
|
654f2ec7ad | ||
|
|
3600e034b8 | ||
|
|
d7c501e175 | ||
|
|
b9e9da579f | ||
|
|
7bea6b4a62 | ||
|
|
ab211266a6 | ||
|
|
4da22a4345 | ||
|
|
fbc1ff62c7 | ||
|
|
1fe72e1a66 | ||
|
|
f82d7610e3 | ||
|
|
bd6ad53875 | ||
|
|
5d09acca4c | ||
|
|
b4e7c4a4cd | ||
|
|
a6269084c0 | ||
|
|
8271d8423d | ||
|
|
c76e29c457 | ||
|
|
4750a7553b | ||
|
|
60786921a9 | ||
|
|
751bbcc127 | ||
|
|
58e8dd5456 | ||
|
|
1586c25847 | ||
|
|
cded5a7948 | ||
|
|
6238e065f2 | ||
|
|
72fbbfdd0d | ||
|
|
2796c1cd4d | ||
|
|
54c9886627 | ||
|
|
05713cb389 | ||
|
|
8bb3ee5f18 | ||
|
|
bc0b4825f1 | ||
|
|
ef7f17abf4 | ||
|
|
876cd21f0b | ||
|
|
5c92d49873 | ||
|
|
da47b573be | ||
|
|
2f04be8778 | ||
|
|
69178ddfd8 | ||
|
|
a310f6fff0 | ||
|
|
7474abe286 | ||
|
|
df2ba3a3c6 | ||
|
|
e536456236 | ||
|
|
8d77122da3 | ||
|
|
fb66effa51 | ||
|
|
5052e71c31 | ||
|
|
bfc78d16ca | ||
|
|
c425e3562b | ||
|
|
1f26092aa9 | ||
|
|
2849bb96f4 | ||
|
|
7b749f2a4c | ||
|
|
8803c94ce0 | ||
|
|
f5235c943b | ||
|
|
59fec889b5 | ||
|
|
f42f04a807 | ||
|
|
51f6f37925 | ||
|
|
9651a4ca98 | ||
|
|
2b7fd36322 | ||
|
|
b8014fd4df | ||
|
|
07460f6e1f | ||
|
|
f7bf3b2afb | ||
|
|
056f298cdf | ||
|
|
e83da53162 | ||
|
|
9f38a47a02 | ||
|
|
236a4d4a6d | ||
|
|
0909471510 | ||
|
|
05eaf59c89 | ||
|
|
7749613801 | ||
|
|
e3dbaedbb4 | ||
|
|
9f17ced6de | ||
|
|
de54ef871d | ||
|
|
b0da45d6d0 | ||
|
|
9b2a46fa92 | ||
|
|
12099d2db6 | ||
|
|
84fa75936a | ||
|
|
d78d8121a1 | ||
|
|
a9a3a52872 | ||
|
|
912e3bdfce | ||
|
|
ee1b25d9e8 | ||
|
|
20ef5e2c18 | ||
|
|
6ee419bc52 | ||
|
|
85b00d3c76 | ||
|
|
bc4ad31d48 | ||
|
|
71aad8ee32 | ||
|
|
8bb8231559 | ||
|
|
3cf9caae89 | ||
|
|
f983c67135 | ||
|
|
f2aef5b93f | ||
|
|
46d4288969 | ||
|
|
65516e872f | ||
|
|
171329246c | ||
|
|
b2bee699e0 | ||
|
|
95c66b4d67 | ||
|
|
babc8feb2b | ||
|
|
2f445c546a | ||
|
|
a0283b3e3e | ||
|
|
61bd156fb0 | ||
|
|
8ad0cf8e5f | ||
|
|
ecd6d70da6 | ||
|
|
359617d927 | ||
|
|
38c286329a | ||
|
|
401b4095cc | ||
|
|
06ab1a8ef0 | ||
|
|
726fb8b015 | ||
|
|
b3b5c7a59f | ||
|
|
d18f934978 | ||
|
|
e67f1f79bc | ||
|
|
e931829411 | ||
|
|
db8ebd606c | ||
|
|
072a358a94 | ||
|
|
6ceb1b150c | ||
|
|
a4e03e1877 | ||
|
|
02b3e4277b | ||
|
|
37daf801e6 | ||
|
|
68d9f7eeb2 | ||
|
|
526e2420ca | ||
|
|
a9cc58fc28 | ||
|
|
77889ce1c6 | ||
|
|
549073119e | ||
|
|
5c5af5795f | ||
|
|
68e10934e4 | ||
|
|
c67bb1444a | ||
|
|
07389a152e | ||
|
|
e562e21555 | ||
|
|
86dfe7dd3f | ||
|
|
ac0a8f3449 | ||
|
|
8e4a63db67 | ||
|
|
c02c63806f | ||
|
|
42e5d7f6e9 | ||
|
|
d8cf1af422 | ||
|
|
9723661c80 | ||
|
|
81cba7ad97 | ||
|
|
c23f58de40 | ||
|
|
2cf67ca7da | ||
|
|
392bd850ea | ||
|
|
3b2ad9d1bd | ||
|
|
27b7474180 | ||
|
|
63948d728e | ||
|
|
d219d3b873 | ||
|
|
93ab290bc1 | ||
|
|
7335c5d79a | ||
|
|
242ead722a | ||
|
|
8a6d9696a8 | ||
|
|
896b7ea242 | ||
|
|
0c7f4c7828 | ||
|
|
3d35af2a87 | ||
|
|
fed3c36f84 | ||
|
|
414d81aa40 | ||
|
|
d548803769 | ||
|
|
1180258394 | ||
|
|
48a566a24b | ||
|
|
3bc5d1df81 | ||
|
|
c7222e2e86 |
2
.github/workflows/dms-stable.yml
vendored
2
.github/workflows/dms-stable.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
4
.github/workflows/go-ci.yml
vendored
4
.github/workflows/go-ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
|
||||
2
.github/workflows/nix-pr-check.yml
vendored
2
.github/workflows/nix-pr-check.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/prek.yml
vendored
4
.github/workflows/prek.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: core/go.mod
|
||||
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v6
|
||||
# with:
|
||||
# token: ${{ steps.app_token.outputs.token }}
|
||||
# fetch-depth: 0
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
TAG: ${{ inputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
git checkout ${TAG}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Download core artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: core-assets-*
|
||||
merge-multiple: true
|
||||
|
||||
4
.github/workflows/run-copr.yml
vendored
4
.github/workflows/run-copr.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
rpm -qpi "$SRPM"
|
||||
|
||||
- name: Upload SRPM artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||
path: ${{ steps.build.outputs.srpm_path }}
|
||||
|
||||
22
.github/workflows/run-obs.yml
vendored
22
.github/workflows/run-obs.yml
vendored
@@ -9,8 +9,8 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -119,9 +119,8 @@ jobs:
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
# Check each stable package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
@@ -140,7 +139,7 @@ jobs:
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
echo "✓ Both packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
@@ -196,10 +195,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Wait before OBS upload
|
||||
run: sleep 3
|
||||
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
@@ -245,7 +247,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -266,7 +268,7 @@ jobs:
|
||||
} > distro/opensuse/dms-git.spec
|
||||
|
||||
- name: Update Debian dms-git changelog version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -345,7 +347,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
@@ -389,7 +391,7 @@ jobs:
|
||||
UPLOADED_PACKAGES=()
|
||||
SKIPPED_PACKAGES=()
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
|
||||
28
.github/workflows/run-ppa.yml
vendored
28
.github/workflows/run-ppa.yml
vendored
@@ -4,9 +4,15 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: "dms-git"
|
||||
description: "Package to upload"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
@@ -25,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -139,7 +145,7 @@ jobs:
|
||||
fi
|
||||
else
|
||||
# Fallback
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -151,12 +157,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
cache: false
|
||||
@@ -209,7 +215,7 @@ jobs:
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms dms-greeter" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
# Map package to PPA name
|
||||
@@ -236,7 +242,11 @@ jobs:
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||
# ppa-upload.sh uploads to questing + resolute when series is omitted
|
||||
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
|
||||
echo "::error::Upload failed for $PKG"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
|
||||
2
.github/workflows/update-vendor-hash.yml
vendored
2
.github/workflows/update-vendor-hash.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.5.0
|
||||
- Overhauled shadows
|
||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
||||
- Greeter stuff
|
||||
- Terminal mux
|
||||
- Locale overrides
|
||||
- new neovim theming
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Overhauled system monitor, graphs, styling
|
||||
|
||||
@@ -86,7 +86,9 @@ touch .qmlls.ini
|
||||
|
||||
4. Restart dms to generate the `.qmlls.ini` file
|
||||
|
||||
5. Make your changes, test, and open a pull request.
|
||||
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
|
||||
|
||||
6. Make your changes, test, and open a pull request.
|
||||
|
||||
### I18n/Localization
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||
ASSETS_DIR=assets
|
||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||
|
||||
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||
|
||||
all: build
|
||||
|
||||
@@ -32,6 +32,9 @@ clean:
|
||||
@$(MAKE) -C $(CORE_DIR) clean
|
||||
@echo "Clean complete"
|
||||
|
||||
lint-qml:
|
||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||
|
||||
# Installation targets
|
||||
install-bin:
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@@ -76,7 +79,7 @@ install-desktop:
|
||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||
@echo "Desktop entry installed"
|
||||
|
||||
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||
@echo ""
|
||||
@echo "Installation complete!"
|
||||
@echo ""
|
||||
@@ -130,6 +133,7 @@ help:
|
||||
@echo " all (default) - Build the DMS binary"
|
||||
@echo " build - Same as 'all'"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||
@echo ""
|
||||
@echo "Install:"
|
||||
@echo " install - Build and install everything (requires sudo)"
|
||||
|
||||
@@ -28,6 +28,12 @@ packages:
|
||||
outpkg: mocks_brightness
|
||||
interfaces:
|
||||
DBusConn:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
|
||||
config:
|
||||
dir: "internal/mocks/geolocation"
|
||||
outpkg: mocks_geolocation
|
||||
interfaces:
|
||||
Client:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||
config:
|
||||
dir: "internal/mocks/network"
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
repos:
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.9.0
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
require_serial: true
|
||||
- id: golangci-lint-full
|
||||
- id: golangci-lint-config-verify
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
name: golangci-lint-fmt
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
|
||||
language: system
|
||||
require_serial: true
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- id: golangci-lint-full
|
||||
name: golangci-lint-full
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
|
||||
language: system
|
||||
require_serial: true
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- id: golangci-lint-config-verify
|
||||
name: golangci-lint-config-verify
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
|
||||
language: system
|
||||
files: \.golangci\.(?:yml|yaml|toml|json)
|
||||
pass_filenames: false
|
||||
- id: go-test
|
||||
name: go test
|
||||
entry: go test ./...
|
||||
|
||||
@@ -63,19 +63,19 @@ endif
|
||||
|
||||
build-all: build dankinstall
|
||||
|
||||
install: build
|
||||
install:
|
||||
@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
|
||||
install-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
|
||||
install-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"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var blurCmd = &cobra.Command{
|
||||
Use: "blur",
|
||||
Short: "Background blur utilities",
|
||||
}
|
||||
|
||||
var blurCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||
Args: cobra.NoArgs,
|
||||
Run: runBlurCheck,
|
||||
}
|
||||
|
||||
func init() {
|
||||
blurCmd.AddCommand(blurCheckCmd)
|
||||
}
|
||||
|
||||
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||
supported, err := blur.ProbeSupport()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch supported {
|
||||
case true:
|
||||
fmt.Println("supported")
|
||||
default:
|
||||
fmt.Println("unsupported")
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,6 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
||||
defer ddc.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||
ddc.WaitPending()
|
||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -525,6 +525,5 @@ func getCommonCommands() []*cobra.Command {
|
||||
configCmd,
|
||||
dlCmd,
|
||||
randrCmd,
|
||||
blurCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
@@ -83,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
|
||||
}
|
||||
|
||||
var (
|
||||
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||
@@ -91,7 +90,6 @@ var (
|
||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
||||
)
|
||||
|
||||
var doctorCmd = &cobra.Command{
|
||||
@@ -470,7 +468,6 @@ func checkWindowManagers() []checkResult {
|
||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
||||
}
|
||||
|
||||
var results []checkResult
|
||||
@@ -503,7 +500,7 @@ func checkWindowManagers() []checkResult {
|
||||
results = append(results, checkResult{
|
||||
catCompositor, "Compositor", statusError,
|
||||
"No supported Wayland compositor found",
|
||||
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
||||
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||
doctorDocsURL + "#compositor-checks",
|
||||
})
|
||||
}
|
||||
@@ -512,24 +509,9 @@ func checkWindowManagers() []checkResult {
|
||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||
}
|
||||
|
||||
results = append(results, checkCompositorBlurSupport())
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func checkCompositorBlurSupport() checkResult {
|
||||
supported, err := blur.ProbeSupport()
|
||||
if err != nil {
|
||||
return checkResult{catCompositor, "Background Blur", statusInfo, "Unable to verify", err.Error(), doctorDocsURL + "#compositor-checks"}
|
||||
}
|
||||
|
||||
if supported {
|
||||
return checkResult{catCompositor, "Background Blur", statusOK, "Supported", "Compositor supports ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
||||
}
|
||||
|
||||
return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
||||
}
|
||||
|
||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||
if err != nil && len(output) == 0 {
|
||||
@@ -553,8 +535,6 @@ func detectRunningWM() string {
|
||||
return "Hyprland"
|
||||
case os.Getenv("NIRI_SOCKET") != "":
|
||||
return "niri"
|
||||
case os.Getenv("MIRACLESOCK") != "":
|
||||
return "Miracle WM"
|
||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||
}
|
||||
@@ -573,7 +553,6 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
||||
qmlContent := `
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
@@ -582,7 +561,6 @@ ShellRoot {
|
||||
property bool idleMonitorAvailable: false
|
||||
property bool idleInhibitorAvailable: false
|
||||
property bool shortcutInhibitorAvailable: false
|
||||
property bool backgroundBlurAvailable: false
|
||||
|
||||
Timer {
|
||||
interval: 50
|
||||
@@ -600,18 +578,16 @@ ShellRoot {
|
||||
|
||||
try {
|
||||
var testItem = Qt.createQmlObject(
|
||||
'import Quickshell; import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined"; ' +
|
||||
'readonly property bool hasBackgroundBlur: typeof BackgroundEffect !== "undefined" ' +
|
||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
||||
'}',
|
||||
root
|
||||
)
|
||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||
root.backgroundBlurAvailable = testItem.hasBackgroundBlur
|
||||
testItem.destroy()
|
||||
} catch (e) {}
|
||||
|
||||
@@ -620,8 +596,6 @@ ShellRoot {
|
||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
||||
|
||||
console.warn(root.backgroundBlurAvailable ? "FEATURE:BackgroundBlur:OK" : "FEATURE:BackgroundBlur:UNAVAILABLE")
|
||||
|
||||
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||
}
|
||||
}
|
||||
@@ -642,7 +616,6 @@ ShellRoot {
|
||||
{"IdleMonitor", "Idle detection"},
|
||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
||||
}
|
||||
|
||||
var results []checkResult
|
||||
@@ -847,14 +820,10 @@ func checkOptionalDependencies() []checkResult {
|
||||
results = append(results, checkImageFormatPlugins()...)
|
||||
|
||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||
return !utils.CommandExists(t)
|
||||
})
|
||||
|
||||
if len(terminals) > 0 {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||
} else {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
||||
}
|
||||
|
||||
networkResult, err := network.DetectNetworkStack()
|
||||
@@ -1110,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
|
||||
if currentCategory != -1 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
||||
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
||||
currentCategory = r.category
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
|
||||
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
|
||||
|
||||
if doctorVerbose && r.details != "" {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
|
||||
fmt.Fprintf(&sb, " - %s\n", r.details)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
|
||||
}
|
||||
|
||||
sb.WriteString("\n---\n")
|
||||
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
|
||||
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -111,37 +109,16 @@ func updateArchLinux() error {
|
||||
}
|
||||
|
||||
var packageName string
|
||||
var isAUR bool
|
||||
if isArchPackageInstalled("dms-shell") {
|
||||
packageName = "dms-shell"
|
||||
if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
} else if isArchPackageInstalled("dms-shell-git") {
|
||||
packageName = "dms-shell-git"
|
||||
isAUR = true
|
||||
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
isAUR = true
|
||||
} else {
|
||||
fmt.Println("Info: No dms-shell package found.")
|
||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
return updateOtherDistros()
|
||||
}
|
||||
|
||||
if !isAUR {
|
||||
fmt.Printf("This will update %s using pacman.\n", packageName)
|
||||
if !confirmUpdate() {
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
|
||||
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
||||
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("dms successfully updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
var helper string
|
||||
var updateCmd *exec.Cmd
|
||||
|
||||
@@ -477,7 +454,11 @@ func updateDMSBinary() error {
|
||||
|
||||
fmt.Printf("Installing to %s...\n", currentPath)
|
||||
|
||||
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
@@ -31,7 +29,7 @@ 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",
|
||||
PreRunE: preRunPrivileged,
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
term, _ := cmd.Flags().GetBool("terminal")
|
||||
@@ -53,10 +51,9 @@ var greeterInstallCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
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",
|
||||
PreRunE: preRunPrivileged,
|
||||
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) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
auth, _ := cmd.Flags().GetBool("auth")
|
||||
@@ -85,7 +82,7 @@ var greeterEnableCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable DMS greeter in greetd config",
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
PreRunE: preRunPrivileged,
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
term, _ := cmd.Flags().GetBool("terminal")
|
||||
@@ -121,7 +118,7 @@ var greeterUninstallCmd = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Remove DMS greeter configuration and restore previous display manager",
|
||||
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
||||
PreRunE: preRunPrivileged,
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
term, _ := cmd.Flags().GetBool("terminal")
|
||||
@@ -291,7 +288,10 @@ func uninstallGreeter(nonInteractive bool) error {
|
||||
}
|
||||
|
||||
fmt.Println("\nDisabling greetd...")
|
||||
if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil {
|
||||
disableCmd := exec.Command("sudo", "systemctl", "disable", "greetd")
|
||||
disableCmd.Stdout = os.Stdout
|
||||
disableCmd.Stderr = os.Stderr
|
||||
if err := disableCmd.Run(); err != nil {
|
||||
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
||||
} else {
|
||||
fmt.Println(" ✓ greetd disabled")
|
||||
@@ -357,10 +357,10 @@ func restorePreDMSGreetdConfig(sudoPassword string) error {
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath); err != nil {
|
||||
if err := runSudoCommand(sudoPassword, "chmod", "644", configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
||||
@@ -388,14 +388,21 @@ command = "agreety --cmd /bin/bash"
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
||||
}
|
||||
_ = privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath)
|
||||
_ = runSudoCommand(sudoPassword, "chmod", "644", configPath)
|
||||
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSudoCommand(_ string, command string, args ...string) error {
|
||||
cmd := exec.Command("sudo", append([]string{command}, args...)...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
||||
func suggestDisplayManagerRestore(nonInteractive bool) {
|
||||
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||
@@ -414,7 +421,10 @@ func suggestDisplayManagerRestore(nonInteractive bool) {
|
||||
|
||||
enableDM := func(dm string) {
|
||||
fmt.Printf(" Enabling %s...\n", dm)
|
||||
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil {
|
||||
cmd := exec.Command("sudo", "systemctl", "enable", "--force", dm)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
||||
@@ -613,7 +623,10 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
|
||||
if response != "n" && response != "no" {
|
||||
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", greeterGroup, currentUser.Username); err != nil {
|
||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
||||
addUserCmd.Stdout = os.Stdout
|
||||
addUserCmd.Stderr = os.Stderr
|
||||
if err := addUserCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||
}
|
||||
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||
@@ -833,19 +846,22 @@ func disableDisplayManager(dmName string) (bool, error) {
|
||||
actionTaken := false
|
||||
|
||||
if state.NeedsDisable {
|
||||
var action, actionVerb string
|
||||
switch state.EnabledState {
|
||||
case "static":
|
||||
var disableCmd *exec.Cmd
|
||||
var actionVerb string
|
||||
|
||||
if state.EnabledState == "static" {
|
||||
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
||||
action = "mask"
|
||||
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
|
||||
actionVerb = "masked"
|
||||
default:
|
||||
} else {
|
||||
fmt.Printf(" Disabling %s...\n", dmName)
|
||||
action = "disable"
|
||||
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
|
||||
actionVerb = "disabled"
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), "", "systemctl", action, dmName); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -886,7 +902,10 @@ func ensureGreetdEnabled() error {
|
||||
|
||||
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||
fmt.Println(" Unmasking greetd...")
|
||||
if err := privesc.Run(context.Background(), "", "systemctl", "unmask", "greetd"); err != nil {
|
||||
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")
|
||||
@@ -898,7 +917,10 @@ func ensureGreetdEnabled() error {
|
||||
fmt.Println(" Enabling greetd service...")
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
||||
enableCmd.Stdout = os.Stdout
|
||||
enableCmd.Stderr = os.Stderr
|
||||
if err := enableCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
|
||||
@@ -928,7 +950,10 @@ func ensureGraphicalTarget() error {
|
||||
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
||||
if currentTargetStr != "graphical.target" {
|
||||
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
||||
if err := privesc.Run(context.Background(), "", "systemctl", "set-default", "graphical.target"); err != nil {
|
||||
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")
|
||||
|
||||
@@ -57,10 +57,11 @@ func init() {
|
||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||
}
|
||||
|
||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
||||
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
||||
}
|
||||
|
||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
@@ -77,6 +78,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||
|
||||
return matugen.Options{
|
||||
StateDir: stateDir,
|
||||
@@ -87,6 +89,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
Mode: matugen.ColorMode(mode),
|
||||
IconTheme: iconTheme,
|
||||
MatugenType: matugenType,
|
||||
Contrast: contrast,
|
||||
RunUserTemplates: runUserTemplates,
|
||||
StockColors: stockColors,
|
||||
SyncModeWithPortal: syncModeWithPortal,
|
||||
@@ -128,6 +131,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||
"skipTemplates": opts.SkipTemplates,
|
||||
"contrast": opts.Contrast,
|
||||
"wait": wait,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ var (
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssNoConfirm bool
|
||||
ssReset bool
|
||||
ssStdout bool
|
||||
)
|
||||
|
||||
@@ -50,8 +52,10 @@ Examples:
|
||||
dms screenshot output -o DP-1 # Specific output
|
||||
dms screenshot window # Focused window (Hyprland)
|
||||
dms screenshot last # Last region (pre-selected)
|
||||
dms screenshot --reset # Reset last region pre-selection
|
||||
dms screenshot --no-clipboard # Save file only
|
||||
dms screenshot --no-file # Clipboard only
|
||||
dms screenshot --no-confirm # Region capture on mouse release
|
||||
dms screenshot --cursor=on # Include cursor
|
||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||
}
|
||||
@@ -119,6 +123,8 @@ func init() {
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||
|
||||
screenshotCmd.AddCommand(ssRegionCmd)
|
||||
@@ -142,6 +148,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config.Clipboard = !ssNoClipboard
|
||||
config.SaveFile = !ssNoFile
|
||||
config.Notify = !ssNoNotify
|
||||
config.NoConfirm = ssNoConfirm
|
||||
config.Reset = ssReset
|
||||
config.Stdout = ssStdout
|
||||
|
||||
if ssOutputDir != "" {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -21,7 +19,7 @@ var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
PersistentPreRunE: preRunPrivileged,
|
||||
PersistentPreRunE: requireMutableSystemCommand,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
log.Fatalf("Error during setup: %v", err)
|
||||
@@ -269,8 +267,6 @@ func runSetupDmsConfig(name string) error {
|
||||
func runSetup() error {
|
||||
fmt.Println("=== DMS Configuration Setup ===")
|
||||
|
||||
ensureInputGroup()
|
||||
|
||||
wm, wmSelected := promptCompositor()
|
||||
terminal, terminalSelected := promptTerminal()
|
||||
useSystemd := promptSystemd()
|
||||
@@ -344,37 +340,6 @@ func runSetup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add user to the input group for the evdev manager for inut state tracking.
|
||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||
func ensureInputGroup() {
|
||||
if !utils.HasGroup("input") {
|
||||
return
|
||||
}
|
||||
currentUser := os.Getenv("USER")
|
||||
if currentUser == "" {
|
||||
currentUser = os.Getenv("LOGNAME")
|
||||
}
|
||||
if currentUser == "" {
|
||||
return
|
||||
}
|
||||
out, err := execGroups(currentUser)
|
||||
if err == nil && strings.Contains(out, "input") {
|
||||
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
|
||||
return
|
||||
}
|
||||
fmt.Println("Adding user to input group for Caps Lock OSD support...")
|
||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
|
||||
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
|
||||
} else {
|
||||
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
func execGroups(user string) (string, error) {
|
||||
out, err := exec.Command("groups", user).Output()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func promptCompositor() (deps.WindowManager, bool) {
|
||||
fmt.Println("Select compositor:")
|
||||
fmt.Println("1) Niri")
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -270,16 +269,3 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||
}
|
||||
|
||||
// preRunPrivileged combines the immutable-system check with a privesc tool
|
||||
// selection prompt (shown only when multiple tools are available and the
|
||||
// $DMS_PRIVESC env var isn't set).
|
||||
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
||||
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -29,9 +28,7 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -26,9 +25,7 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,9 @@ func runShellInteractive(session bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
@@ -432,6 +435,9 @@ func runShellDaemon(session bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
|
||||
@@ -7,22 +7,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||
// safe to run as root (e.g. shell completion, help).
|
||||
func isReadOnlyCommand(args []string) bool {
|
||||
for _, arg := range args[1:] {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
continue
|
||||
}
|
||||
switch arg {
|
||||
case "completion", "help", "__complete":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isArchPackageInstalled(packageName string) bool {
|
||||
cmd := exec.Command("pacman", "-Q", packageName)
|
||||
err := cmd.Run()
|
||||
|
||||
10
core/go.mod
10
core/go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/AvengeMedia/DankMaterialShell/core
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
toolchain go1.26.1
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
@@ -16,6 +18,8 @@ require (
|
||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
@@ -32,15 +36,19 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
)
|
||||
|
||||
12
core/go.sum
12
core/go.sum
@@ -58,6 +58,8 @@ 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/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
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=
|
||||
@@ -75,6 +77,8 @@ github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
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=
|
||||
@@ -115,6 +119,8 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
|
||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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=
|
||||
@@ -142,6 +148,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package blur
|
||||
|
||||
import (
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||
|
||||
func ProbeSupport() (bool, error) {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer display.Context().Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
found := false
|
||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case extBackgroundEffectInterface:
|
||||
found = true
|
||||
}
|
||||
})
|
||||
|
||||
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return found, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -12,166 +13,100 @@ import (
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||
|
||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||
func MaybeServeAndExit() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mimeType := os.Getenv(envMime)
|
||||
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||
cachePath := os.Getenv(envCacheFile)
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case cachePath != "":
|
||||
data, err = os.ReadFile(cachePath)
|
||||
os.Remove(cachePath)
|
||||
default:
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func Copy(data []byte, mimeType string) error {
|
||||
return copyForkCached(data, mimeType, false)
|
||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||
}
|
||||
|
||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
return serveClipboard(data, mimeType, pasteOnce)
|
||||
return copyServeWithWriter(func(writer io.Writer) error {
|
||||
total := 0
|
||||
for total < len(data) {
|
||||
n, err := writer.Write(data[total:])
|
||||
total += n
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if total != len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}, mimeType, pasteOnce)
|
||||
}
|
||||
return copyForkCached(data, mimeType, pasteOnce)
|
||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
buf, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read source: %w", err)
|
||||
}
|
||||
return serveClipboard(buf, mimeType, pasteOnce)
|
||||
if !foreground {
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||
cmd := exec.Command(os.Args[0])
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
cmd.Env = append(os.Environ(),
|
||||
envServe+"=1",
|
||||
envMime+"="+mimeType,
|
||||
)
|
||||
if pasteOnce {
|
||||
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||
}
|
||||
cmd.Env = append(cmd.Env, extra...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func waitReady(cmd *exec.Cmd) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||
cacheFile, err := createClipboardCacheFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cache file: %w", err)
|
||||
}
|
||||
cachePath := cacheFile.Name()
|
||||
|
||||
if _, err := cacheFile.Write(data); err != nil {
|
||||
cacheFile.Close()
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("write cache file: %w", err)
|
||||
}
|
||||
if err := cacheFile.Close(); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("close cache file: %w", err)
|
||||
}
|
||||
|
||||
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||
cmd.Stdin = nil
|
||||
if err := waitReady(cmd); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return copyServeReader(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
cmd := newForkCmd(mimeType, pasteOnce)
|
||||
|
||||
switch src := data.(type) {
|
||||
case *os.File:
|
||||
cmd.Stdin = src
|
||||
return waitReady(cmd)
|
||||
|
||||
default:
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
}
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if stdinSource, ok := data.(*os.File); ok {
|
||||
cmd.Stdin = stdinSource
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func signalReady() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
return
|
||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
cachedData, err := createClipboardCacheFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
||||
}
|
||||
os.Stdout.Write([]byte{1})
|
||||
defer os.Remove(cachedData.Name())
|
||||
|
||||
if _, err := io.Copy(cachedData, data); err != nil {
|
||||
return fmt.Errorf("cache clipboard data: %w", err)
|
||||
}
|
||||
if err := cachedData.Close(); err != nil {
|
||||
return fmt.Errorf("close temp cache file: %w", err)
|
||||
}
|
||||
|
||||
return copyServeWithWriter(func(writer io.Writer) error {
|
||||
cachedFile, err := os.Open(cachedData.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("open temp cache file: %w", err)
|
||||
}
|
||||
defer cachedFile.Close()
|
||||
|
||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
||||
return fmt.Errorf("write clipboard data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func createClipboardCacheFile() (*os.File, error) {
|
||||
@@ -194,7 +129,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
||||
return os.CreateTemp("", "dms-clipboard-*")
|
||||
}
|
||||
|
||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
@@ -236,10 +171,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
@@ -278,12 +215,18 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
_, _ = file.Write(data)
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -299,12 +242,13 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
signalReady()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
@@ -558,10 +502,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
@@ -589,12 +535,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
|
||||
if data, ok := offerMap[e.MimeType]; ok {
|
||||
_, _ = file.Write(data)
|
||||
file.Write(data)
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
||||
|
||||
# === Sizing & Layout ===
|
||||
bind = SUPER, R, layoutmsg, togglesplit
|
||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||
bind = SUPER CTRL, F, resizeactive, exact 100%
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||
|
||||
@@ -94,7 +94,6 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
||||
windowrule = tile on, match:class ^(pavucontrol)$
|
||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||
|
||||
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
||||
windowrule = float on, match:class ^(gnome-calculator)$
|
||||
windowrule = float on, match:class ^(galculator)$
|
||||
windowrule = float on, match:class ^(blueman-manager)$
|
||||
|
||||
@@ -224,7 +224,6 @@ window-rule {
|
||||
open-floating false
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^org\.gnome\.Calculator$"#
|
||||
match app-id=r#"^gnome-calculator$"#
|
||||
match app-id=r#"^galculator$"#
|
||||
match app-id=r#"^blueman-manager$"#
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -243,7 +242,11 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||
if a.packageInstalled("dms-shell-bin") {
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
@@ -293,7 +296,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
|
||||
LogOutput: "Installing base-devel development tools",
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||
}
|
||||
@@ -325,13 +328,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
||||
|
||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||
|
||||
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
|
||||
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
|
||||
}
|
||||
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
|
||||
}
|
||||
|
||||
// Phase 3: System Packages
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
@@ -449,37 +445,6 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
||||
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if a.packageInstalled("quickshell-git") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.packageInstalled("quickshell") {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.15,
|
||||
Step: "Removing stable quickshell...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
||||
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
||||
}
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.18,
|
||||
Step: "Building quickshell-git before system packages...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
|
||||
}
|
||||
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
@@ -488,9 +453,6 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||
if slices.Contains(packages, "dms-shell") {
|
||||
args = append(args, "--assume-installed", "dms-shell-compositor=1")
|
||||
}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
@@ -502,7 +464,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
@@ -578,7 +540,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
||||
var dmsShell []string
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "dms-shell-git" {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
dmsShell = append(dmsShell, pkg)
|
||||
} else {
|
||||
isDep := false
|
||||
@@ -659,7 +621,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
if pkg == "dms-shell-git" {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
depsToRemove := []string{
|
||||
"depends = quickshell",
|
||||
@@ -682,7 +644,15 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
{
|
||||
if pkg == "dms-shell-bin" {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||
}
|
||||
} else {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||
@@ -769,9 +739,42 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
CommandInfo: "sudo pacman -U built-package",
|
||||
}
|
||||
|
||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||
var files []string
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
// For DMS split packages, install base package
|
||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil {
|
||||
for _, match := range matches {
|
||||
basename := filepath.Base(match)
|
||||
// Always include base package
|
||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
||||
files = append(files, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also update compositor-specific packages if they're installed
|
||||
if strings.HasSuffix(pkg, "-git") {
|
||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
||||
files = append(files, hyprlandMatches[0])
|
||||
}
|
||||
}
|
||||
if a.packageInstalled("dms-shell-niri-git") {
|
||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
||||
files = append(files, niriMatches[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other packages, install all built packages
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("no package files found after building %s", pkg)
|
||||
@@ -780,7 +783,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||
installArgs = append(installArgs, files...)
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||
|
||||
fileNames := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||
)
|
||||
|
||||
@@ -56,6 +55,27 @@ func (b *BaseDistribution) logError(message string, err error) {
|
||||
b.log(errorMsg)
|
||||
}
|
||||
|
||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||
func escapeSingleQuotes(s string) string {
|
||||
return strings.ReplaceAll(s, "'", "'\\''")
|
||||
}
|
||||
|
||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||
// This helper escapes special characters in the password to prevent shell injection
|
||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||
}
|
||||
|
||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists(name) {
|
||||
@@ -690,7 +710,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
||||
}
|
||||
|
||||
// Install to /usr/local/bin
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -104,15 +103,6 @@ func debianPackageInstalledPrecisely(pkg string) bool {
|
||||
return strings.TrimSpace(string(output)) == "installed"
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func debianRepoArchitecture(arch string) string {
|
||||
switch arch {
|
||||
case "amd64", "x86_64":
|
||||
@@ -192,7 +182,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
LogOutput: "Updating APT package lists",
|
||||
}
|
||||
|
||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
@@ -209,7 +199,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||
}
|
||||
@@ -225,7 +215,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
@@ -451,7 +441,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||
|
||||
// Create keyrings directory if it doesn't exist
|
||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||
if err := mkdirCmd.Run(); err != nil {
|
||||
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||
}
|
||||
@@ -465,7 +455,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
}
|
||||
|
||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
@@ -481,7 +471,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||
}
|
||||
|
||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||
@@ -501,7 +491,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||
}
|
||||
@@ -547,7 +537,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||
}
|
||||
|
||||
@@ -635,7 +625,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, depList...)
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||
}
|
||||
|
||||
@@ -653,7 +643,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
||||
CommandInfo: "sudo apt-get install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
@@ -692,7 +682,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
||||
CommandInfo: "sudo apt-get install golang-go",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -255,7 +254,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError("failed to install prerequisites", err)
|
||||
@@ -438,7 +437,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -462,7 +461,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||
}
|
||||
|
||||
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -538,7 +537,7 @@ func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []st
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
var GentooGlobalUseFlags = []string{
|
||||
@@ -202,9 +201,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if hasUse {
|
||||
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||
} else {
|
||||
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
@@ -282,7 +281,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||
}
|
||||
|
||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||
if syncErr != nil {
|
||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||
@@ -303,7 +302,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.logError("failed to install prerequisites", err)
|
||||
@@ -504,14 +503,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||
packageUseDir := "/etc/portage/package.use"
|
||||
|
||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||
@@ -525,7 +524,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
||||
if checkExistingCmd.Run() == nil {
|
||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||
@@ -533,7 +532,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
||||
}
|
||||
}
|
||||
|
||||
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||
|
||||
output, err := appendCmd.CombinedOutput()
|
||||
@@ -558,7 +557,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
||||
}
|
||||
|
||||
// Enable GURU repository
|
||||
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||
output, err := enableCmd.CombinedOutput()
|
||||
|
||||
@@ -590,7 +589,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
||||
LogOutput: "Syncing GURU repository",
|
||||
}
|
||||
|
||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||
|
||||
@@ -623,7 +622,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
||||
|
||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||
|
||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||
@@ -637,7 +636,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
||||
if checkExistingCmd.Run() == nil {
|
||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||
@@ -645,7 +644,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
||||
}
|
||||
}
|
||||
|
||||
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||
|
||||
output, err := appendCmd.CombinedOutput()
|
||||
@@ -696,6 +695,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
// ManualPackageInstaller provides methods for installing packages from source
|
||||
@@ -144,7 +143,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||
installCmd.Dir = tmpDir
|
||||
if err := installCmd.Run(); err != nil {
|
||||
m.logError("failed to install dgop", err)
|
||||
@@ -214,7 +213,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
||||
CommandInfo: "dpkg -i niri.deb",
|
||||
}
|
||||
|
||||
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||
|
||||
output, err := installDebCmd.CombinedOutput()
|
||||
@@ -325,7 +324,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
||||
CommandInfo: "sudo cmake --install build",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||
installCmd.Dir = tmpDir
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
@@ -388,7 +387,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||
installCmd.Dir = tmpDir
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||
@@ -454,7 +453,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
@@ -493,11 +492,16 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||
}
|
||||
|
||||
@@ -642,11 +646,15 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -250,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
|
||||
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.logError("failed to install prerequisites", err)
|
||||
@@ -426,7 +426,7 @@ func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string
|
||||
|
||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||
for _, pkg := range extraPkgs {
|
||||
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
||||
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||
@@ -507,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
||||
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||
}
|
||||
|
||||
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||
}
|
||||
@@ -588,7 +588,7 @@ func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sud
|
||||
}
|
||||
|
||||
for _, alias := range aliases {
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
||||
repoOutput, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||
@@ -646,7 +646,7 @@ func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packag
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
||||
CommandInfo: "sudo cmake --install build",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||
installCmd.Dir = tmpDir
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
@@ -798,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
||||
CommandInfo: "sudo zypper install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -178,7 +177,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
LogOutput: "Updating APT package lists",
|
||||
}
|
||||
|
||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
@@ -196,7 +195,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
// Not installed, install it
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||
}
|
||||
@@ -212,7 +211,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
@@ -399,7 +398,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
|
||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y software-properties-common")
|
||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||
@@ -417,7 +416,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||
@@ -438,7 +437,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||
}
|
||||
@@ -505,7 +504,7 @@ func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []st
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||
}
|
||||
|
||||
@@ -592,7 +591,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, depList...)
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||
}
|
||||
|
||||
@@ -610,7 +609,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
||||
CommandInfo: "sudo apt-get install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
@@ -650,7 +649,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||
}
|
||||
|
||||
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||
@@ -665,7 +664,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||
}
|
||||
@@ -679,7 +678,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
||||
CommandInfo: "sudo apt-get install golang-go",
|
||||
}
|
||||
|
||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||
}
|
||||
|
||||
|
||||
42
core/internal/geolocation/client.go
Normal file
42
core/internal/geolocation/client.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package geolocation
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
|
||||
func NewClient() Client {
|
||||
geoclueClient, err := newGeoClueClient()
|
||||
if err != nil {
|
||||
log.Warnf("GeoClue2 unavailable: %v", err)
|
||||
return newSeededIpClient()
|
||||
}
|
||||
|
||||
loc, _ := geoclueClient.GetLocation()
|
||||
if loc.Latitude != 0 || loc.Longitude != 0 {
|
||||
log.Info("Using GeoClue2 location")
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
log.Info("GeoClue2 has no fix yet, seeding with IP location")
|
||||
ipLoc, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
log.Warnf("IP location seed failed: %v", err)
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
log.Info("Seeded GeoClue2 with IP location")
|
||||
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
func newSeededIpClient() *IpClient {
|
||||
client := newIpClient()
|
||||
ipLoc, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
log.Warnf("IP location also failed: %v", err)
|
||||
return client
|
||||
}
|
||||
|
||||
log.Info("Using IP location")
|
||||
client.currLocation.Latitude = ipLoc.Latitude
|
||||
client.currLocation.Longitude = ipLoc.Longitude
|
||||
return client
|
||||
}
|
||||
243
core/internal/geolocation/client_geoclue.go
Normal file
243
core/internal/geolocation/client_geoclue.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package geolocation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusGeoClueService = "org.freedesktop.GeoClue2"
|
||||
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
|
||||
dbusGeoClueInterface = dbusGeoClueService
|
||||
|
||||
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
|
||||
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
|
||||
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
|
||||
|
||||
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
|
||||
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
|
||||
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
|
||||
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
|
||||
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
|
||||
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
|
||||
|
||||
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
|
||||
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
|
||||
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
|
||||
)
|
||||
|
||||
type GeoClueClient struct {
|
||||
currLocation *Location
|
||||
locationMutex sync.RWMutex
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
clientPath dbus.ObjectPath
|
||||
signals chan *dbus.Signal
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
|
||||
subscribers syncmap.Map[string, chan Location]
|
||||
}
|
||||
|
||||
func newGeoClueClient() (*GeoClueClient, error) {
|
||||
dbusConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
||||
}
|
||||
|
||||
c := &GeoClueClient{
|
||||
dbusConn: dbusConn,
|
||||
stopChan: make(chan struct{}),
|
||||
signals: make(chan *dbus.Signal, 256),
|
||||
|
||||
currLocation: &Location{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.setupClient(); err != nil {
|
||||
dbusConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.startSignalPump(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Close() {
|
||||
close(c.stopChan)
|
||||
|
||||
c.sigWG.Wait()
|
||||
|
||||
if c.signals != nil {
|
||||
c.dbusConn.RemoveSignal(c.signals)
|
||||
close(c.signals)
|
||||
}
|
||||
|
||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||
close(ch)
|
||||
c.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
if c.dbusConn != nil {
|
||||
c.dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Subscribe(id string) chan Location {
|
||||
ch := make(chan Location, 64)
|
||||
c.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Unsubscribe(id string) {
|
||||
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) setupClient() error {
|
||||
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
|
||||
|
||||
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
|
||||
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
|
||||
}
|
||||
|
||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
|
||||
return fmt.Errorf("failed to set desktop ID: %w", err)
|
||||
}
|
||||
|
||||
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
|
||||
return fmt.Errorf("failed to set time threshold: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) startSignalPump() error {
|
||||
c.dbusConn.Signal(c.signals)
|
||||
|
||||
if err := c.dbusConn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(c.clientPath),
|
||||
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
||||
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sigWG.Add(1)
|
||||
go func() {
|
||||
defer c.sigWG.Done()
|
||||
|
||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||
clientObj.Call(dbusGeoClueClientTimeStart, 0)
|
||||
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
return
|
||||
case sig, ok := <-c.signals:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c.handleSignal(sig)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
|
||||
switch sig.Name {
|
||||
case dbusGeoClueClientLocationUpdated:
|
||||
if len(sig.Body) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.handleLocationUpdated(newLocationPath); err != nil {
|
||||
log.Warn("GeoClue: Failed to handle location update: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
|
||||
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
|
||||
|
||||
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.locationMutex.Lock()
|
||||
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
|
||||
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
|
||||
c.locationMutex.Unlock()
|
||||
|
||||
c.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) notifySubscribers() {
|
||||
currentLocation, err := c.GetLocation()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||
select {
|
||||
case ch <- currentLocation:
|
||||
default:
|
||||
log.Warn("GeoClue: subscriber channel full, dropping update")
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) SeedLocation(loc Location) {
|
||||
c.locationMutex.Lock()
|
||||
defer c.locationMutex.Unlock()
|
||||
c.currLocation.Latitude = loc.Latitude
|
||||
c.currLocation.Longitude = loc.Longitude
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) GetLocation() (Location, error) {
|
||||
c.locationMutex.RLock()
|
||||
defer c.locationMutex.RUnlock()
|
||||
if c.currLocation == nil {
|
||||
return Location{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
}, nil
|
||||
}
|
||||
stateCopy := *c.currLocation
|
||||
return stateCopy, nil
|
||||
}
|
||||
91
core/internal/geolocation/client_ip.go
Normal file
91
core/internal/geolocation/client_ip.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package geolocation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IpClient struct {
|
||||
currLocation *Location
|
||||
}
|
||||
|
||||
type ipLocationResult struct {
|
||||
Location
|
||||
City string
|
||||
}
|
||||
|
||||
type ipAPIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
func newIpClient() *IpClient {
|
||||
return &IpClient{
|
||||
currLocation: &Location{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IpClient) Subscribe(id string) chan Location {
|
||||
ch := make(chan Location, 1)
|
||||
if location, err := c.GetLocation(); err == nil {
|
||||
ch <- location
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *IpClient) Unsubscribe(id string) {}
|
||||
|
||||
func (c *IpClient) Close() {}
|
||||
|
||||
func (c *IpClient) GetLocation() (Location, error) {
|
||||
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
|
||||
return *c.currLocation, nil
|
||||
}
|
||||
|
||||
result, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
|
||||
c.currLocation.Latitude = result.Latitude
|
||||
c.currLocation.Longitude = result.Longitude
|
||||
return *c.currLocation, nil
|
||||
}
|
||||
|
||||
func fetchIPLocation() (ipLocationResult, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
resp, err := client.Get("http://ip-api.com/json/")
|
||||
if err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var data ipAPIResponse
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
|
||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
|
||||
}
|
||||
|
||||
return ipLocationResult{
|
||||
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
|
||||
City: data.City,
|
||||
}, nil
|
||||
}
|
||||
15
core/internal/geolocation/types.go
Normal file
15
core/internal/geolocation/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package geolocation
|
||||
|
||||
type Location struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
GetLocation() (Location, error)
|
||||
|
||||
Subscribe(id string) chan Location
|
||||
Unsubscribe(id string)
|
||||
|
||||
Close()
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/sblinch/kdl-go"
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
@@ -346,17 +345,56 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyArch:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm greetd")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"pacman -S --needed --noconfirm greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyFedora:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y greetd")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"dnf install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilySUSE:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y greetd")
|
||||
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"zypper install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyUbuntu:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyDebian:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyGentoo:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"emerge --ask n sys-apps/greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyNix:
|
||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
||||
}
|
||||
@@ -417,56 +455,56 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
||||
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
||||
if _, err := exec.LookPath("gpg"); err != nil {
|
||||
logFunc("Installing gnupg for OBS repository key import...")
|
||||
installGPGCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y gnupg")
|
||||
installGPGCmd := exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "gnupg")
|
||||
installGPGCmd.Stdout = os.Stdout
|
||||
installGPGCmd.Stderr = os.Stderr
|
||||
if err := installGPGCmd.Run(); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
||||
}
|
||||
}
|
||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||
mkdirCmd := exec.CommandContext(ctx, "sudo", "mkdir", "-p", "/etc/apt/keyrings")
|
||||
mkdirCmd.Stdout = os.Stdout
|
||||
mkdirCmd.Stderr = os.Stderr
|
||||
mkdirCmd.Run()
|
||||
addKeyCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf(`bash -c "curl -fsSL %s | gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg"`, keyURL))
|
||||
addKeyCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL))
|
||||
addKeyCmd.Stdout = os.Stdout
|
||||
addKeyCmd.Stderr = os.Stderr
|
||||
addKeyCmd.Run()
|
||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf(`bash -c "echo '%s' > /etc/apt/sources.list.d/danklinux.list"`, repoLine))
|
||||
addRepoCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine))
|
||||
addRepoCmd.Stdout = os.Stdout
|
||||
addRepoCmd.Stderr = os.Stderr
|
||||
addRepoCmd.Run()
|
||||
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
||||
case distros.FamilySUSE:
|
||||
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
||||
logFunc("Adding DankLinux OBS repository...")
|
||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo %s", repoURL))
|
||||
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL)
|
||||
addRepoCmd.Stdout = os.Stdout
|
||||
addRepoCmd.Stderr = os.Stderr
|
||||
addRepoCmd.Run()
|
||||
privesc.ExecCommand(ctx, sudoPassword, "zypper refresh").Run()
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y dms-greeter")
|
||||
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter")
|
||||
case distros.FamilyUbuntu:
|
||||
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
||||
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
||||
ppacmd := privesc.ExecCommand(ctx, sudoPassword, "add-apt-repository -y ppa:avengemedia/danklinux")
|
||||
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux")
|
||||
ppacmd.Stdout = os.Stdout
|
||||
ppacmd.Stderr = os.Stderr
|
||||
ppacmd.Run()
|
||||
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
||||
case distros.FamilyFedora:
|
||||
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
||||
logFunc("Enabling COPR avengemedia/danklinux...")
|
||||
coprcmd := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y avengemedia/danklinux")
|
||||
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux")
|
||||
coprcmd.Stdout = os.Stdout
|
||||
coprcmd.Stderr = os.Stderr
|
||||
coprcmd.Run()
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y dms-greeter")
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter")
|
||||
case distros.FamilyArch:
|
||||
aurHelper := ""
|
||||
for _, helper := range []string{"paru", "yay"} {
|
||||
@@ -519,25 +557,25 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
||||
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
||||
action = "Updated"
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
||||
}
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err == nil {
|
||||
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
||||
} else {
|
||||
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
||||
} else {
|
||||
logFunc("✓ Restored SELinux context for dms-greeter")
|
||||
@@ -563,7 +601,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to stat cache directory: %w", err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
created = true
|
||||
@@ -575,35 +613,34 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||
daemonUser := DetectGreeterUser()
|
||||
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
||||
owner := preferredOwner
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||
// Some setups may not have a matching daemon user at this moment; fall back
|
||||
// to root:<group> while still allowing group-writable greeter runtime access.
|
||||
fallbackOwner := fmt.Sprintf("root:%s", group)
|
||||
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
||||
}
|
||||
owner = fallbackOwner
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||
}
|
||||
|
||||
runtimeDirs := []string{
|
||||
filepath.Join(cacheDir, ".local"),
|
||||
filepath.Join(cacheDir, ".local", "state"),
|
||||
filepath.Join(cacheDir, ".local", "state", "wireplumber"),
|
||||
filepath.Join(cacheDir, ".local", "share"),
|
||||
filepath.Join(cacheDir, ".cache"),
|
||||
}
|
||||
for _, dir := range runtimeDirs {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, dir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil {
|
||||
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", dir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil {
|
||||
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
@@ -615,7 +652,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
|
||||
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
||||
}
|
||||
}
|
||||
@@ -640,13 +677,13 @@ func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPat
|
||||
info, err := os.Lstat(legacyPath)
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
||||
if copyErr := privesc.Run(context.Background(), sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
||||
}
|
||||
|
||||
@@ -673,7 +710,7 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
||||
}
|
||||
|
||||
@@ -690,15 +727,15 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
||||
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
||||
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
||||
}
|
||||
|
||||
if utils.CommandExists("apparmor_parser") {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
||||
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
||||
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
||||
} else {
|
||||
@@ -745,10 +782,10 @@ func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) err
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to write PAM config: %w", err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to set PAM config permissions: %w", err)
|
||||
}
|
||||
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
|
||||
@@ -769,9 +806,9 @@ func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
|
||||
if utils.CommandExists("apparmor_parser") {
|
||||
_ = privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
||||
_ = runSudoCmd(sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
||||
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
||||
}
|
||||
logFunc(" ✓ Removed DMS AppArmor profile")
|
||||
@@ -801,17 +838,50 @@ func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyArch:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyFedora:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y acl")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilySUSE:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y acl")
|
||||
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyUbuntu:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyDebian:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
||||
}
|
||||
|
||||
case distros.FamilyGentoo:
|
||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
|
||||
}
|
||||
|
||||
case distros.FamilyNix:
|
||||
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
||||
}
|
||||
@@ -868,7 +938,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
|
||||
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
||||
continue
|
||||
@@ -925,7 +995,7 @@ func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
|
||||
continue
|
||||
}
|
||||
for _, user := range existingUsers {
|
||||
_ = privesc.Run(context.Background(), sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||
cleaned = true
|
||||
}
|
||||
}
|
||||
@@ -965,7 +1035,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
||||
if !utils.HasGroup(group) {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "groupadd", "-r", group); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil {
|
||||
return fmt.Errorf("failed to create %s group: %w", group, err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
||||
@@ -976,7 +1046,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||
} else {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||
@@ -991,7 +1061,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
if strings.Contains(string(daemonGroupsOutput), group) {
|
||||
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
||||
} else {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
||||
@@ -1021,12 +1091,12 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
@@ -1238,8 +1308,8 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat
|
||||
}
|
||||
|
||||
target := filepath.Join(cacheDir, "colors.json")
|
||||
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", target)
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", source, target); err != nil {
|
||||
_ = runSudoCmd(sudoPassword, "rm", "-f", target)
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", source, target); err != nil {
|
||||
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
||||
}
|
||||
|
||||
@@ -1291,9 +1361,9 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
||||
}
|
||||
}
|
||||
|
||||
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", link.target)
|
||||
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
||||
}
|
||||
|
||||
@@ -1335,13 +1405,13 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
||||
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
||||
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||
if state.ResolvedGreeterWallpaperPath == "" {
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
||||
}
|
||||
logFunc("✓ Cleared greeter wallpaper override")
|
||||
return nil
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||
}
|
||||
src := state.ResolvedGreeterWallpaperPath
|
||||
@@ -1352,17 +1422,17 @@ func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPas
|
||||
if st.IsDir() {
|
||||
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", src, destPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil {
|
||||
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||
}
|
||||
greeterGroup := DetectGreeterGroup()
|
||||
daemonUser := DetectGreeterUser()
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||
}
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", destPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||
}
|
||||
logFunc("✓ Synced greeter wallpaper override")
|
||||
@@ -1727,10 +1797,10 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
||||
}
|
||||
if wantFprint || wantU2f {
|
||||
@@ -1789,13 +1859,13 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
greeterDir := "/etc/greetd/niri"
|
||||
greeterGroup := DetectGreeterGroup()
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
||||
}
|
||||
|
||||
@@ -1817,7 +1887,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
||||
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
||||
}
|
||||
|
||||
@@ -1840,7 +1910,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
||||
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
||||
}
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
||||
}
|
||||
|
||||
@@ -1916,7 +1986,7 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
|
||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
||||
return fmt.Errorf("failed to update greetd config: %w", err)
|
||||
}
|
||||
|
||||
@@ -1932,10 +2002,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
|
||||
}
|
||||
|
||||
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", path, backupPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return privesc.Run(context.Background(), sudoPassword, "chmod", "644", backupPath)
|
||||
return runSudoCmd(sudoPassword, "chmod", "644", backupPath)
|
||||
}
|
||||
|
||||
func (s *niriGreeterSync) processFile(filePath string) error {
|
||||
@@ -2148,8 +2218,6 @@ vt = 1
|
||||
|
||||
wrapperCmd := resolveGreeterWrapperPath()
|
||||
|
||||
// Build command based on compositor and dms path
|
||||
// When dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
|
||||
compositorLower := strings.ToLower(compositor)
|
||||
commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
|
||||
if dmsPath != "" {
|
||||
@@ -2173,11 +2241,11 @@ vt = 1
|
||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
||||
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
||||
}
|
||||
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
||||
return fmt.Errorf("failed to install greetd config: %w", err)
|
||||
}
|
||||
|
||||
@@ -2281,6 +2349,27 @@ func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
|
||||
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
||||
}
|
||||
|
||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if sudoPassword != "" {
|
||||
fullArgs := append([]string{command}, args...)
|
||||
quotedArgs := make([]string, len(fullArgs))
|
||||
for i, arg := range fullArgs {
|
||||
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
||||
}
|
||||
cmdStr := strings.Join(quotedArgs, " ")
|
||||
|
||||
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
||||
} else {
|
||||
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func checkSystemdEnabled(service string) (string, error) {
|
||||
cmd := exec.Command("systemctl", "is-enabled", service)
|
||||
output, _ := cmd.Output()
|
||||
@@ -2297,7 +2386,7 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
||||
switch state {
|
||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "disable", dm); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "disable", dm); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
||||
@@ -2318,13 +2407,13 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
||||
}
|
||||
if state == "masked" || state == "masked-runtime" {
|
||||
logFunc(" Unmasking greetd...")
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||
}
|
||||
logFunc(" ✓ Unmasked greetd")
|
||||
}
|
||||
logFunc(" Enabling greetd service (--force)...")
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||
}
|
||||
logFunc("✓ greetd enabled")
|
||||
@@ -2344,7 +2433,7 @@ func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
|
||||
return nil
|
||||
}
|
||||
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
||||
return fmt.Errorf("failed to set graphical target: %w", err)
|
||||
}
|
||||
logFunc("✓ Default target set to graphical.target")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
"github.com/sblinch/kdl-go"
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
)
|
||||
|
||||
@@ -291,7 +292,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
||||
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||
parser.currentSource = overridePath
|
||||
|
||||
doc, err := parseKDL(data)
|
||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -50,103 +50,6 @@ type NiriParser struct {
|
||||
conflictingConfigs map[string]*NiriKeyBinding
|
||||
}
|
||||
|
||||
func parseKDL(data []byte) (*document.Document, error) {
|
||||
return kdl.Parse(strings.NewReader(normalizeKDLBraces(string(data))))
|
||||
}
|
||||
|
||||
func normalizeKDLBraces(input string) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(input))
|
||||
|
||||
var prev byte
|
||||
n := len(input)
|
||||
for i := 0; i < n; {
|
||||
c := input[i]
|
||||
|
||||
switch {
|
||||
case c == '"':
|
||||
end := findStringEnd(input, i)
|
||||
sb.WriteString(input[i:end])
|
||||
prev = '"'
|
||||
i = end
|
||||
case c == '/' && i+1 < n && input[i+1] == '/':
|
||||
end := findLineCommentEnd(input, i)
|
||||
sb.WriteString(input[i:end])
|
||||
prev = '\n'
|
||||
i = end
|
||||
case c == '/' && i+1 < n && input[i+1] == '*':
|
||||
end := findBlockCommentEnd(input, i)
|
||||
sb.WriteString(input[i:end])
|
||||
prev = '/'
|
||||
i = end
|
||||
case c == '{' && prev != 0 && !isBraceAdjacentSpace(prev):
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteByte(c)
|
||||
prev = c
|
||||
i++
|
||||
default:
|
||||
sb.WriteByte(c)
|
||||
prev = c
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func findStringEnd(s string, start int) int {
|
||||
n := len(s)
|
||||
for i := start + 1; i < n; {
|
||||
switch s[i] {
|
||||
case '\\':
|
||||
i += 2
|
||||
case '"':
|
||||
return i + 1
|
||||
default:
|
||||
i++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func findLineCommentEnd(s string, start int) int {
|
||||
for i := start + 2; i < len(s); i++ {
|
||||
if s[i] == '\n' {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func findBlockCommentEnd(s string, start int) int {
|
||||
n := len(s)
|
||||
depth := 1
|
||||
for i := start + 2; i < n && depth > 0; {
|
||||
switch {
|
||||
case i+1 < n && s[i] == '/' && s[i+1] == '*':
|
||||
depth++
|
||||
i += 2
|
||||
case i+1 < n && s[i] == '*' && s[i+1] == '/':
|
||||
depth--
|
||||
i += 2
|
||||
if depth == 0 {
|
||||
return i
|
||||
}
|
||||
default:
|
||||
i++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func isBraceAdjacentSpace(b byte) bool {
|
||||
switch b {
|
||||
case ' ', '\t', '\n', '\r', '{':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewNiriParser(configDir string) *NiriParser {
|
||||
return &NiriParser{
|
||||
configDir: configDir,
|
||||
@@ -188,7 +91,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := parseKDL(data)
|
||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -256,7 +159,7 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
||||
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
doc, err := parseKDL(data)
|
||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
@@ -3,74 +3,9 @@ package providers
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
||||
config := `recent-windows {
|
||||
binds {
|
||||
Alt+Tab { next-window scope="output"; }
|
||||
Alt+Shift+Tab { previous-window scope="output"; }
|
||||
Alt+grave { next-window filter="app-id"; }
|
||||
Alt+Shift+grave { previous-window filter="app-id"; }
|
||||
Alt+Escape { next-window scope="all"; }
|
||||
Alt+Shift+Escape{ previous-window scope="all"; }
|
||||
}
|
||||
}
|
||||
`
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
result, err := ParseNiriKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseNiriKeys failed on valid niri config: %v", err)
|
||||
}
|
||||
|
||||
var found *NiriKeyBinding
|
||||
for i := range result.Section.Keybinds {
|
||||
kb := &result.Section.Keybinds[i]
|
||||
if kb.Key == "Escape" && slices.Contains(kb.Mods, "Alt") && slices.Contains(kb.Mods, "Shift") {
|
||||
found = kb
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatal("Alt+Shift+Escape bind missing — '{' without preceding space was not handled")
|
||||
}
|
||||
if found.Action != "previous-window" {
|
||||
t.Errorf("Action = %q, want %q", found.Action, "previous-window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeKDLBraces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"already spaced", "node { child }\n", "node { child }\n"},
|
||||
{"missing space", "node{ child }\n", "node { child }\n"},
|
||||
{"niri keybind", "Alt+Shift+Escape{ previous-window; }", "Alt+Shift+Escape { previous-window; }"},
|
||||
{"brace inside string", `node "a{b" { child }`, `node "a{b" { child }`},
|
||||
{"brace in line comment", "// foo{bar\nnode { }", "// foo{bar\nnode { }"},
|
||||
{"brace in block comment", "/* foo{bar */ node{ }", "/* foo{bar */ node { }"},
|
||||
{"escaped quote in string", `node "a\"b{c" { }`, `node "a\"b{c" { }`},
|
||||
{"leading brace", "{ child }", "{ child }"},
|
||||
{"nested missing space", "a{b{ c }}", "a {b { c }}"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := normalizeKDLBraces(tc.in)
|
||||
if got != tc.out {
|
||||
t.Errorf("normalizeKDLBraces(%q) = %q, want %q", tc.in, got, tc.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNiriParseKeyCombo(t *testing.T) {
|
||||
tests := []struct {
|
||||
combo string
|
||||
|
||||
@@ -99,6 +99,7 @@ type Options struct {
|
||||
Mode ColorMode
|
||||
IconTheme string
|
||||
MatugenType string
|
||||
Contrast float64
|
||||
RunUserTemplates bool
|
||||
ColorsOnly bool
|
||||
StockColors string
|
||||
@@ -228,6 +229,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
log.Info("Running matugen color hex with stock color overrides")
|
||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = appendContrastArg(args, opts.Contrast)
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
@@ -264,6 +266,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
args = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = appendContrastArg(args, opts.Contrast)
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
@@ -299,6 +302,13 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func appendContrastArg(args []string, contrast float64) []string {
|
||||
if contrast == 0 {
|
||||
return args
|
||||
}
|
||||
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||
|
||||
@@ -687,6 +697,7 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||
baseArgs = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||
if flags.isV4 {
|
||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||
}
|
||||
|
||||
203
core/internal/mocks/geolocation/mock_Client.go
Normal file
203
core/internal/mocks/geolocation/mock_Client.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package mocks_geolocation
|
||||
|
||||
import (
|
||||
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockClient is an autogenerated mock type for the Client type
|
||||
type MockClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockClient) EXPECT() *MockClient_Expecter {
|
||||
return &MockClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with no fields
|
||||
func (_m *MockClient) Close() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockClient_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
|
||||
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetLocation provides a mock function with no fields
|
||||
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetLocation")
|
||||
}
|
||||
|
||||
var r0 geolocation.Location
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(geolocation.Location)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
|
||||
type MockClient_GetLocation_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetLocation is a helper method to define mock.On call
|
||||
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
|
||||
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Subscribe provides a mock function with given fields: id
|
||||
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Subscribe")
|
||||
}
|
||||
|
||||
var r0 chan geolocation.Location
|
||||
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(chan geolocation.Location)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
|
||||
type MockClient_Subscribe_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Subscribe is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
|
||||
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Unsubscribe provides a mock function with given fields: id
|
||||
func (_m *MockClient) Unsubscribe(id string) {
|
||||
_m.Called(id)
|
||||
}
|
||||
|
||||
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
|
||||
type MockClient_Unsubscribe_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Unsubscribe is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
|
||||
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockClient {
|
||||
mock := &MockClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1062,6 +1062,62 @@ func (_c *MockBackend_GetWiFiNetworkDetails_Call) RunAndReturn(run func(string)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetWiFiQRCodeContent provides a mock function with given fields: ssid
|
||||
func (_m *MockBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
ret := _m.Called(ssid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetWiFiQRCodeContent")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(ssid)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(ssid)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(ssid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockBackend_GetWiFiQRCodeContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiQRCodeContent'
|
||||
type MockBackend_GetWiFiQRCodeContent_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetWiFiQRCodeContent is a helper method to define mock.On call
|
||||
// - ssid string
|
||||
func (_e *MockBackend_Expecter) GetWiFiQRCodeContent(ssid interface{}) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
return &MockBackend_GetWiFiQRCodeContent_Call{Call: _e.mock.On("GetWiFiQRCodeContent", ssid)}
|
||||
}
|
||||
|
||||
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Run(run func(ssid string)) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Return(_a0 string, _a1 error) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBackend_GetWiFiQRCodeContent_Call) RunAndReturn(run func(string) (string, error)) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetWiredConnections provides a mock function with no fields
|
||||
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
package privesc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Tool identifies a privilege-escalation binary.
|
||||
type Tool string
|
||||
|
||||
const (
|
||||
ToolSudo Tool = "sudo"
|
||||
ToolDoas Tool = "doas"
|
||||
ToolRun0 Tool = "run0"
|
||||
)
|
||||
|
||||
// EnvVar selects a specific tool when set to one of: sudo, doas, run0.
|
||||
const EnvVar = "DMS_PRIVESC"
|
||||
|
||||
var detectionOrder = []Tool{ToolSudo, ToolDoas, ToolRun0}
|
||||
|
||||
var (
|
||||
detectOnce sync.Once
|
||||
detected Tool
|
||||
detectErr error
|
||||
userSelected bool
|
||||
)
|
||||
|
||||
// Detect returns the tool that should be used for privilege escalation.
|
||||
// The result is cached after the first call.
|
||||
func Detect() (Tool, error) {
|
||||
detectOnce.Do(func() {
|
||||
detected, detectErr = detectTool()
|
||||
})
|
||||
return detected, detectErr
|
||||
}
|
||||
|
||||
// ResetForTesting clears cached detection state.
|
||||
func ResetForTesting() {
|
||||
detectOnce = sync.Once{}
|
||||
detected = ""
|
||||
detectErr = nil
|
||||
userSelected = false
|
||||
}
|
||||
|
||||
// AvailableTools returns the set of supported tools that are installed on
|
||||
// PATH, in detection-precedence order.
|
||||
func AvailableTools() []Tool {
|
||||
var out []Tool
|
||||
for _, t := range detectionOrder {
|
||||
if t.Available() {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EnvOverride returns the tool selected by the $DMS_PRIVESC env var (if any)
|
||||
// along with ok=true when the variable is set. An empty or unset variable
|
||||
// returns ok=false.
|
||||
func EnvOverride() (Tool, bool) {
|
||||
v := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar)))
|
||||
if v == "" {
|
||||
return "", false
|
||||
}
|
||||
return Tool(v), true
|
||||
}
|
||||
|
||||
// SetTool forces the detected tool to t, bypassing autodetection. Intended
|
||||
// for use after the caller has prompted the user for a selection.
|
||||
func SetTool(t Tool) error {
|
||||
if !t.Available() {
|
||||
return fmt.Errorf("%q is not installed", t.Name())
|
||||
}
|
||||
detectOnce = sync.Once{}
|
||||
detectOnce.Do(func() {
|
||||
detected = t
|
||||
detectErr = nil
|
||||
})
|
||||
userSelected = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectTool() (Tool, error) {
|
||||
switch override := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar))); override {
|
||||
case "":
|
||||
// fall through to autodetect
|
||||
case string(ToolSudo), string(ToolDoas), string(ToolRun0):
|
||||
t := Tool(override)
|
||||
if !t.Available() {
|
||||
return "", fmt.Errorf("%s=%s but %q is not installed", EnvVar, override, t.Name())
|
||||
}
|
||||
return t, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid %s=%q: must be one of sudo, doas, run0", EnvVar, override)
|
||||
}
|
||||
|
||||
for _, t := range detectionOrder {
|
||||
if t.Available() {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no supported privilege escalation tool found (tried: sudo, doas, run0)")
|
||||
}
|
||||
|
||||
// Name returns the binary name.
|
||||
func (t Tool) Name() string { return string(t) }
|
||||
|
||||
// Available reports whether this tool's binary is on PATH.
|
||||
func (t Tool) Available() bool {
|
||||
if t == "" {
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath(string(t))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SupportsStdinPassword reports whether the tool can accept a password via
|
||||
// stdin. Only sudo (-S) supports this.
|
||||
func (t Tool) SupportsStdinPassword() bool {
|
||||
return t == ToolSudo
|
||||
}
|
||||
|
||||
// EscapeSingleQuotes escapes single quotes for safe inclusion inside a
|
||||
// bash single-quoted string.
|
||||
func EscapeSingleQuotes(s string) string {
|
||||
return strings.ReplaceAll(s, "'", "'\\''")
|
||||
}
|
||||
|
||||
// MakeCommand returns a bash command string that runs `command` with the
|
||||
// detected tool. When the tool supports stdin passwords and password is
|
||||
// non-empty, the password is piped in. Otherwise the tool is invoked with
|
||||
// no non-interactive flag so that an interactive TTY prompt is still
|
||||
// possible for CLI callers.
|
||||
//
|
||||
// If detection fails, the returned shell string exits 1 with an error
|
||||
// message so callers that treat the *exec.Cmd as infallible still fail
|
||||
// deterministically.
|
||||
func MakeCommand(password, command string) string {
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return failingShell(err)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case ToolSudo:
|
||||
if password != "" {
|
||||
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
|
||||
}
|
||||
return fmt.Sprintf("sudo %s", command)
|
||||
case ToolDoas:
|
||||
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
|
||||
case ToolRun0:
|
||||
return fmt.Sprintf("run0 sh -c '%s'", EscapeSingleQuotes(command))
|
||||
default:
|
||||
return failingShell(fmt.Errorf("unsupported privilege tool: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
// ExecCommand builds an exec.Cmd that runs `command` as root via the
|
||||
// detected tool. Detection errors surface at Run() time as a failing
|
||||
// command writing a clear error to stderr.
|
||||
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command))
|
||||
}
|
||||
|
||||
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
|
||||
// No stdin password is supplied; callers relying on non-interactive success
|
||||
// should ensure cached credentials are present (see CheckCached).
|
||||
func ExecArgv(ctx context.Context, argv ...string) *exec.Cmd {
|
||||
if len(argv) == 0 {
|
||||
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("privesc.ExecArgv: argv must not be empty")))
|
||||
}
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
|
||||
}
|
||||
|
||||
switch t {
|
||||
case ToolSudo, ToolDoas:
|
||||
return exec.CommandContext(ctx, string(t), argv...)
|
||||
case ToolRun0:
|
||||
return exec.CommandContext(ctx, "run0", argv...)
|
||||
default:
|
||||
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("unsupported privilege tool: %q", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func failingShell(err error) string {
|
||||
return fmt.Sprintf("printf 'privesc: %%s\\n' '%s' >&2; exit 1", EscapeSingleQuotes(err.Error()))
|
||||
}
|
||||
|
||||
// CheckCached runs a non-interactive credential probe. Returns nil if the
|
||||
// tool will run commands without prompting (cached credentials, nopass, or
|
||||
// polkit rule).
|
||||
func CheckCached(ctx context.Context) error {
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch t {
|
||||
case ToolSudo:
|
||||
cmd = exec.CommandContext(ctx, "sudo", "-n", "true")
|
||||
case ToolDoas:
|
||||
cmd = exec.CommandContext(ctx, "doas", "-n", "true")
|
||||
case ToolRun0:
|
||||
cmd = exec.CommandContext(ctx, "run0", "--no-ask-password", "true")
|
||||
default:
|
||||
return fmt.Errorf("unsupported privilege tool: %q", t)
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ClearCache invalidates any cached credentials. No-op for tools that do
|
||||
// not expose a cache-clear operation.
|
||||
func ClearCache(ctx context.Context) error {
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t {
|
||||
case ToolSudo:
|
||||
return exec.CommandContext(ctx, "sudo", "-k").Run()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateWithAskpass validates cached credentials using an askpass helper
|
||||
// script. Only sudo supports this mechanism; the TUI uses it to trigger
|
||||
// fingerprint authentication via PAM.
|
||||
func ValidateWithAskpass(ctx context.Context, askpassScript string) error {
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t != ToolSudo {
|
||||
return fmt.Errorf("askpass validation requires sudo (detected: %s)", t)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ValidatePassword validates the given password. Only sudo supports this
|
||||
// (via `sudo -S -v`); for other tools the caller should fall back to
|
||||
// CheckCached.
|
||||
func ValidatePassword(ctx context.Context, password string) error {
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t != ToolSudo {
|
||||
return fmt.Errorf("password validation requires sudo (detected: %s)", t)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(stdin, "%s\n", password); err != nil {
|
||||
stdin.Close()
|
||||
_ = cmd.Wait()
|
||||
return err
|
||||
}
|
||||
stdin.Close()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
// QuoteArgsForShell wraps each argv element in single quotes so the result
|
||||
// can be safely passed to bash -c.
|
||||
func QuoteArgsForShell(argv []string) string {
|
||||
parts := make([]string, len(argv))
|
||||
for i, a := range argv {
|
||||
parts[i] = "'" + EscapeSingleQuotes(a) + "'"
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// Run invokes argv with privilege escalation. When the tool supports stdin
|
||||
// passwords and password is non-empty, the password is piped in. Otherwise
|
||||
// argv is invoked directly, which may prompt on a TTY.
|
||||
// Stdout and Stderr are inherited from the current process.
|
||||
func Run(ctx context.Context, password string, argv ...string) error {
|
||||
if len(argv) == 0 {
|
||||
return fmt.Errorf("privesc.Run: argv must not be empty")
|
||||
}
|
||||
t, err := Detect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch {
|
||||
case t == ToolSudo && password != "":
|
||||
cmd = ExecCommand(ctx, password, QuoteArgsForShell(argv))
|
||||
default:
|
||||
cmd = ExecArgv(ctx, argv...)
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// stdinIsTTY reports whether stdin is a character device (interactive
|
||||
// terminal) rather than a pipe or file.
|
||||
func stdinIsTTY() bool {
|
||||
fi, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
// PromptCLI interactively prompts the user to pick a privilege tool when more
|
||||
// than one is installed and $DMS_PRIVESC is not set. If stdin is not a TTY,
|
||||
// or only one tool is available, or the env var is set, the detected tool is
|
||||
// returned without any prompt.
|
||||
//
|
||||
// The prompt is written to out (typically os.Stdout/os.Stderr) and input is
|
||||
// read from in. EOF or empty input selects the first option.
|
||||
func PromptCLI(out io.Writer, in io.Reader) (Tool, error) {
|
||||
if userSelected {
|
||||
return Detect()
|
||||
}
|
||||
if _, envSet := EnvOverride(); envSet {
|
||||
return Detect()
|
||||
}
|
||||
|
||||
tools := AvailableTools()
|
||||
switch len(tools) {
|
||||
case 0:
|
||||
return "", fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||
case 1:
|
||||
if err := SetTool(tools[0]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tools[0], nil
|
||||
}
|
||||
|
||||
if !stdinIsTTY() {
|
||||
return Detect()
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, "Multiple privilege escalation tools detected:")
|
||||
for i, t := range tools {
|
||||
fmt.Fprintf(out, " [%d] %s\n", i+1, t.Name())
|
||||
}
|
||||
fmt.Fprintf(out, "Choose one [1-%d] (default 1, or set %s=<tool> to skip): ", len(tools), EnvVar)
|
||||
|
||||
reader := bufio.NewReader(in)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return "", fmt.Errorf("failed to read selection: %w", err)
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
idx := 1
|
||||
if line != "" {
|
||||
n, convErr := strconv.Atoi(line)
|
||||
if convErr != nil || n < 1 || n > len(tools) {
|
||||
return "", fmt.Errorf("invalid selection %q", line)
|
||||
}
|
||||
idx = n
|
||||
}
|
||||
|
||||
chosen := tools[idx-1]
|
||||
if err := SetTool(chosen); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return chosen, nil
|
||||
}
|
||||
@@ -444,21 +444,20 @@ func GetFocusedMonitor() string {
|
||||
|
||||
type outputInfo struct {
|
||||
x, y int32
|
||||
scale float64
|
||||
transform int32
|
||||
}
|
||||
|
||||
func getAllOutputInfos() map[string]*outputInfo {
|
||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
ctx := display.Context()
|
||||
defer ctx.Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||
@@ -477,17 +476,16 @@ func getAllOutputInfos() map[string]*outputInfo {
|
||||
})
|
||||
|
||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if outputManager == nil {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type headState struct {
|
||||
name string
|
||||
x, y int32
|
||||
scale float64
|
||||
transform int32
|
||||
}
|
||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||
@@ -503,9 +501,6 @@ func getAllOutputInfos() map[string]*outputInfo {
|
||||
state.x = pe.X
|
||||
state.y = pe.Y
|
||||
})
|
||||
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||
state.scale = se.Scale
|
||||
})
|
||||
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||
state.transform = te.Transform
|
||||
})
|
||||
@@ -516,32 +511,21 @@ func getAllOutputInfos() map[string]*outputInfo {
|
||||
|
||||
for !done {
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
result := make(map[string]*outputInfo, len(heads))
|
||||
for _, state := range heads {
|
||||
if state.name == "" {
|
||||
continue
|
||||
}
|
||||
result[state.name] = &outputInfo{
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
scale: state.scale,
|
||||
transform: state.transform,
|
||||
if state.name == outputName {
|
||||
return &outputInfo{
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
transform: state.transform,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
infos := getAllOutputInfos()
|
||||
if infos == nil {
|
||||
return nil, false
|
||||
}
|
||||
info, ok := infos[outputName]
|
||||
return info, ok
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||
|
||||
@@ -113,7 +113,11 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||
}
|
||||
|
||||
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||
r.preSelect = GetLastRegion()
|
||||
if r.screenshoter != nil && r.screenshoter.config.Reset {
|
||||
r.preSelect = Region{}
|
||||
} else {
|
||||
r.preSelect = GetLastRegion()
|
||||
}
|
||||
|
||||
if err := r.connect(); err != nil {
|
||||
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||
|
||||
@@ -114,6 +114,9 @@ func (r *RegionSelector) setupPointerHandlers() {
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
|
||||
r.finishSelection()
|
||||
}
|
||||
}
|
||||
default:
|
||||
r.cancelled = true
|
||||
|
||||
@@ -138,9 +138,13 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
|
||||
if !r.showCapturedCursor {
|
||||
cursorLabel = "show"
|
||||
}
|
||||
captureKey := "Space/Enter"
|
||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
|
||||
captureKey = "Drag+Release"
|
||||
}
|
||||
|
||||
items := []struct{ key, desc string }{
|
||||
{"Space/Enter", "capture"},
|
||||
{captureKey, "capture"},
|
||||
{"P", cursorLabel + " cursor"},
|
||||
{"Esc", "cancel"},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package screenshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -107,6 +106,12 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||
if s.config.Reset {
|
||||
if err := SaveLastRegion(Region{}); err != nil {
|
||||
log.Debug("failed to reset last region", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
selector := NewRegionSelector(s)
|
||||
result, cancelled, err := selector.Run()
|
||||
if err != nil {
|
||||
@@ -299,20 +304,22 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
if len(outputs) == 0 {
|
||||
return nil, fmt.Errorf("no outputs available")
|
||||
}
|
||||
|
||||
if len(outputs) == 1 {
|
||||
return s.captureWholeOutput(outputs[0])
|
||||
}
|
||||
|
||||
wlrInfos := getAllOutputInfos()
|
||||
|
||||
type pendingOutput struct {
|
||||
// Capture all outputs first to get actual buffer sizes
|
||||
type capturedOutput struct {
|
||||
output *WaylandOutput
|
||||
result *CaptureResult
|
||||
logX float64
|
||||
logY float64
|
||||
scale float64
|
||||
physX int
|
||||
physY int
|
||||
}
|
||||
var pending []pendingOutput
|
||||
maxScale := 1.0
|
||||
captured := make([]capturedOutput, 0, len(outputs))
|
||||
|
||||
var minX, minY, maxX, maxY int
|
||||
first := true
|
||||
|
||||
for _, output := range outputs {
|
||||
result, err := s.captureWholeOutput(output)
|
||||
@@ -321,74 +328,50 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
logX, logY := float64(output.x), float64(output.y)
|
||||
outX, outY := output.x, output.y
|
||||
scale := float64(output.scale)
|
||||
|
||||
switch DetectCompositor() {
|
||||
case CompositorHyprland:
|
||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||
logX, logY = float64(hx), float64(hy)
|
||||
outX, outY = hx, hy
|
||||
}
|
||||
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||
scale = hs
|
||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||
scale = s
|
||||
}
|
||||
default:
|
||||
if wlrInfos != nil {
|
||||
if info, ok := wlrInfos[output.name]; ok {
|
||||
logX, logY = float64(info.x), float64(info.y)
|
||||
if info.scale > 0 {
|
||||
scale = info.scale
|
||||
}
|
||||
}
|
||||
case CompositorDWL:
|
||||
if info, ok := getOutputInfo(output.name); ok {
|
||||
outX, outY = info.x, info.y
|
||||
}
|
||||
}
|
||||
|
||||
if scale <= 0 {
|
||||
scale = 1.0
|
||||
}
|
||||
|
||||
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||
if scale > maxScale {
|
||||
maxScale = scale
|
||||
}
|
||||
}
|
||||
physX := int(float64(outX) * scale)
|
||||
physY := int(float64(outY) * scale)
|
||||
|
||||
if len(pending) == 0 {
|
||||
return nil, fmt.Errorf("failed to capture any outputs")
|
||||
}
|
||||
if len(pending) == 1 {
|
||||
return pending[0].result, nil
|
||||
}
|
||||
captured = append(captured, capturedOutput{
|
||||
output: output,
|
||||
result: result,
|
||||
physX: physX,
|
||||
physY: physY,
|
||||
})
|
||||
|
||||
type layoutEntry struct {
|
||||
result *CaptureResult
|
||||
canvasX int
|
||||
canvasY int
|
||||
canvasW int
|
||||
canvasH int
|
||||
}
|
||||
entries := make([]layoutEntry, len(pending))
|
||||
var minX, minY, maxX, maxY int
|
||||
right := physX + result.Buffer.Width
|
||||
bottom := physY + result.Buffer.Height
|
||||
|
||||
for i, p := range pending {
|
||||
cx := int(math.Round(p.logX * maxScale))
|
||||
cy := int(math.Round(p.logY * maxScale))
|
||||
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
||||
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
||||
|
||||
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
||||
|
||||
right := cx + cw
|
||||
bottom := cy + ch
|
||||
if i == 0 {
|
||||
minX, minY, maxX, maxY = cx, cy, right, bottom
|
||||
if first {
|
||||
minX, minY = physX, physY
|
||||
maxX, maxY = right, bottom
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
if cx < minX {
|
||||
minX = cx
|
||||
|
||||
if physX < minX {
|
||||
minX = physX
|
||||
}
|
||||
if cy < minY {
|
||||
minY = cy
|
||||
if physY < minY {
|
||||
minY = physY
|
||||
}
|
||||
if right > maxX {
|
||||
maxX = right
|
||||
@@ -398,26 +381,35 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(captured) == 0 {
|
||||
return nil, fmt.Errorf("failed to capture any outputs")
|
||||
}
|
||||
|
||||
if len(captured) == 1 {
|
||||
return captured[0].result, nil
|
||||
}
|
||||
|
||||
totalW := maxX - minX
|
||||
totalH := maxY - minY
|
||||
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
||||
|
||||
compositeStride := totalW * 4
|
||||
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
||||
if err != nil {
|
||||
for _, e := range entries {
|
||||
e.result.Buffer.Close()
|
||||
for _, c := range captured {
|
||||
c.result.Buffer.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||
}
|
||||
|
||||
composite.Clear()
|
||||
|
||||
var format uint32
|
||||
for _, e := range entries {
|
||||
for _, c := range captured {
|
||||
if format == 0 {
|
||||
format = e.result.Format
|
||||
format = c.result.Format
|
||||
}
|
||||
s.blitBufferScaled(composite, e.result.Buffer,
|
||||
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||
e.result.YInverted)
|
||||
e.result.Buffer.Close()
|
||||
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
||||
c.result.Buffer.Close()
|
||||
}
|
||||
|
||||
return &CaptureResult{
|
||||
@@ -427,44 +419,32 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
||||
if dstW <= 0 || dstH <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||
srcData := src.Data()
|
||||
dstData := dst.Data()
|
||||
|
||||
for dy := 0; dy < dstH; dy++ {
|
||||
canvasY := dstY + dy
|
||||
if canvasY < 0 || canvasY >= dst.Height {
|
||||
continue
|
||||
}
|
||||
|
||||
srcY := dy * src.Height / dstH
|
||||
for srcY := 0; srcY < src.Height; srcY++ {
|
||||
actualSrcY := srcY
|
||||
if yInverted {
|
||||
srcY = src.Height - 1 - srcY
|
||||
actualSrcY = src.Height - 1 - srcY
|
||||
}
|
||||
if srcY < 0 || srcY >= src.Height {
|
||||
|
||||
dy := dstY + srcY
|
||||
if dy < 0 || dy >= dst.Height {
|
||||
continue
|
||||
}
|
||||
|
||||
srcRowOff := srcY * src.Stride
|
||||
dstRowOff := canvasY * dst.Stride
|
||||
srcRowOff := actualSrcY * src.Stride
|
||||
dstRowOff := dy * dst.Stride
|
||||
|
||||
for dx := 0; dx < dstW; dx++ {
|
||||
canvasX := dstX + dx
|
||||
if canvasX < 0 || canvasX >= dst.Width {
|
||||
continue
|
||||
}
|
||||
|
||||
srcX := dx * src.Width / dstW
|
||||
if srcX >= src.Width {
|
||||
for srcX := 0; srcX < src.Width; srcX++ {
|
||||
dx := dstX + srcX
|
||||
if dx < 0 || dx >= dst.Width {
|
||||
continue
|
||||
}
|
||||
|
||||
si := srcRowOff + srcX*4
|
||||
di := dstRowOff + canvasX*4
|
||||
di := dstRowOff + dx*4
|
||||
|
||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||
continue
|
||||
|
||||
@@ -52,6 +52,8 @@ type Config struct {
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
NoConfirm bool
|
||||
Reset bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
@@ -66,6 +68,8 @@ func DefaultConfig() Config {
|
||||
return Config{
|
||||
Mode: ModeRegion,
|
||||
Cursor: CursorOff,
|
||||
NoConfirm: false,
|
||||
Reset: false,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
|
||||
@@ -215,34 +215,31 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
if existing, exists := b.debounceTimers[id]; exists {
|
||||
if existing.Stop() {
|
||||
b.debounceWg.Done()
|
||||
}
|
||||
if timer, exists := b.debounceTimers[id]; exists {
|
||||
timer.Reset(200 * time.Millisecond)
|
||||
} else {
|
||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||
b.debounceMutex.Lock()
|
||||
pending, exists := b.debouncePending[id]
|
||||
if exists {
|
||||
delete(b.debouncePending, id)
|
||||
}
|
||||
b.debounceMutex.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
err := b.setBrightnessImmediateWithExponent(id, pending.percent)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||
}
|
||||
|
||||
if pending.callback != nil {
|
||||
pending.callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
b.debounceWg.Add(1)
|
||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||
defer b.debounceWg.Done()
|
||||
|
||||
b.debounceMutex.Lock()
|
||||
pending, hasPending := b.debouncePending[id]
|
||||
delete(b.debouncePending, id)
|
||||
delete(b.debounceTimers, id)
|
||||
b.debounceMutex.Unlock()
|
||||
|
||||
if !hasPending {
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.setBrightnessImmediateWithExponent(id, pending.percent); err != nil {
|
||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||
}
|
||||
|
||||
if pending.callback != nil {
|
||||
pending.callback()
|
||||
}
|
||||
})
|
||||
b.debounceMutex.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -493,19 +490,5 @@ func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
|
||||
return percent
|
||||
}
|
||||
|
||||
func (b *DDCBackend) WaitPending() {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
b.debounceWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Debug("WaitPending timed out waiting for DDC writes")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *DDCBackend) Close() {
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ type DDCBackend struct {
|
||||
debounceMutex sync.Mutex
|
||||
debounceTimers map[string]*time.Timer
|
||||
debouncePending map[string]ddcPendingSet
|
||||
debounceWg sync.WaitGroup
|
||||
}
|
||||
|
||||
type ddcPendingSet struct {
|
||||
|
||||
@@ -6,12 +6,20 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/pilebones/go-udev/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
udevRecvBufSize = 8 * 1024 * 1024
|
||||
udevMaxRetries = 5
|
||||
udevBaseDelay = 2 * time.Second
|
||||
udevMaxDelay = 60 * time.Second
|
||||
)
|
||||
|
||||
type UdevMonitor struct {
|
||||
stop chan struct{}
|
||||
rescanMutex sync.Mutex
|
||||
@@ -29,13 +37,6 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
||||
}
|
||||
|
||||
func (m *UdevMonitor) run(manager *Manager) {
|
||||
conn := &netlink.UEventConn{}
|
||||
if err := conn.Connect(netlink.UdevEvent); err != nil {
|
||||
log.Errorf("Failed to connect to udev netlink: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
matcher := &netlink.RuleDefinitions{
|
||||
Rules: []netlink.RuleDefinition{
|
||||
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||
@@ -48,6 +49,46 @@ func (m *UdevMonitor) run(manager *Manager) {
|
||||
return
|
||||
}
|
||||
|
||||
failures := 0
|
||||
for {
|
||||
if err := m.monitorLoop(manager, matcher); err != nil {
|
||||
log.Errorf("Udev monitor error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
failures++
|
||||
if failures > udevMaxRetries {
|
||||
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
|
||||
return
|
||||
}
|
||||
|
||||
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
|
||||
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
|
||||
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
|
||||
conn := &netlink.UEventConn{}
|
||||
if err := conn.Connect(netlink.UdevEvent); err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
|
||||
log.Warnf("Failed to set udev socket receive buffer: %v", err)
|
||||
}
|
||||
|
||||
events := make(chan netlink.UEvent)
|
||||
errs := make(chan error)
|
||||
conn.Monitor(events, errs, matcher)
|
||||
@@ -57,10 +98,9 @@ func (m *UdevMonitor) run(manager *Manager) {
|
||||
for {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
return nil
|
||||
case err := <-errs:
|
||||
log.Errorf("Udev monitor error: %v", err)
|
||||
return
|
||||
return err
|
||||
case event := <-events:
|
||||
m.handleEvent(manager, event)
|
||||
}
|
||||
|
||||
@@ -212,10 +212,9 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
}
|
||||
|
||||
var offer any
|
||||
switch {
|
||||
case e.Id != nil:
|
||||
if e.Id != nil {
|
||||
offer = e.Id
|
||||
case e.OfferId != 0:
|
||||
} else if e.OfferId != 0 {
|
||||
m.offerMutex.RLock()
|
||||
offer = m.offerRegistry[e.OfferId]
|
||||
m.offerMutex.RUnlock()
|
||||
@@ -225,6 +224,10 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
wasOwner := m.isOwner
|
||||
m.ownerLock.Unlock()
|
||||
|
||||
if offer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if wasOwner {
|
||||
return
|
||||
}
|
||||
@@ -233,11 +236,9 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
m.currentOffer = offer
|
||||
|
||||
if prevOffer != nil && prevOffer != offer {
|
||||
m.releaseOffer(prevOffer)
|
||||
}
|
||||
|
||||
if offer == nil {
|
||||
return
|
||||
m.offerMutex.Lock()
|
||||
delete(m.offerMimeTypes, prevOffer)
|
||||
m.offerMutex.Unlock()
|
||||
}
|
||||
|
||||
m.offerMutex.RLock()
|
||||
@@ -291,33 +292,6 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
log.Info("Data device setup complete")
|
||||
}
|
||||
|
||||
func (m *Manager) releaseOffer(offer any) {
|
||||
if offer == nil {
|
||||
return
|
||||
}
|
||||
typedOffer, ok := offer.(*ext_data_control.ExtDataControlOfferV1)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
m.offerMutex.Lock()
|
||||
delete(m.offerMimeTypes, offer)
|
||||
delete(m.offerRegistry, typedOffer.ID())
|
||||
m.offerMutex.Unlock()
|
||||
typedOffer.Destroy()
|
||||
}
|
||||
|
||||
func (m *Manager) releaseCurrentSource() {
|
||||
if m.currentSource == nil {
|
||||
return
|
||||
}
|
||||
source, ok := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||
m.currentSource = nil
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
source.Destroy()
|
||||
}
|
||||
|
||||
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
||||
defer r.Close()
|
||||
|
||||
@@ -421,7 +395,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
if extractHash(v) != hash {
|
||||
continue
|
||||
}
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
@@ -439,7 +413,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
@@ -482,14 +456,6 @@ func encodeEntry(e Entry) ([]byte, error) {
|
||||
}
|
||||
|
||||
func decodeEntry(data []byte) (Entry, error) {
|
||||
return decodeEntryFields(data, true)
|
||||
}
|
||||
|
||||
func decodeEntryMeta(data []byte) (Entry, error) {
|
||||
return decodeEntryFields(data, false)
|
||||
}
|
||||
|
||||
func decodeEntryFields(data []byte, withData bool) (Entry, error) {
|
||||
buf := bytes.NewReader(data)
|
||||
var e Entry
|
||||
|
||||
@@ -497,15 +463,8 @@ func decodeEntryFields(data []byte, withData bool) (Entry, error) {
|
||||
|
||||
var dataLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||
switch {
|
||||
case withData:
|
||||
e.Data = make([]byte, dataLen)
|
||||
buf.Read(e.Data)
|
||||
default:
|
||||
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
|
||||
return e, err
|
||||
}
|
||||
}
|
||||
e.Data = make([]byte, dataLen)
|
||||
buf.Read(e.Data)
|
||||
|
||||
var mimeLen uint32
|
||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||
@@ -709,9 +668,14 @@ func sizeStr(size int) string {
|
||||
func (m *Manager) updateState() {
|
||||
history := m.GetHistory()
|
||||
|
||||
for i := range history {
|
||||
history[i].Data = nil
|
||||
}
|
||||
|
||||
var current *Entry
|
||||
if len(history) > 0 {
|
||||
c := history[0]
|
||||
c.Data = nil
|
||||
current = &c
|
||||
}
|
||||
|
||||
@@ -786,7 +750,7 @@ func (m *Manager) GetHistory() []Entry {
|
||||
c := b.Cursor()
|
||||
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -971,7 +935,7 @@ func (m *Manager) ClearHistory() {
|
||||
var toDelete [][]byte
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil || !entry.Pinned {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
@@ -994,7 +958,7 @@ func (m *Manager) ClearHistory() {
|
||||
if b != nil {
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, _ := decodeEntryMeta(v)
|
||||
entry, _ := decodeEntry(v)
|
||||
if entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
@@ -1102,7 +1066,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
||||
m.ownerLock.Unlock()
|
||||
})
|
||||
|
||||
m.releaseCurrentSource()
|
||||
m.currentSource = source
|
||||
m.sourceMutex.Lock()
|
||||
m.sourceMimeTypes = []string{mimeType}
|
||||
@@ -1182,11 +1145,9 @@ func (m *Manager) Close() {
|
||||
m.subscribers = make(map[string]chan State)
|
||||
m.subMutex.Unlock()
|
||||
|
||||
m.releaseCurrentSource()
|
||||
|
||||
if m.currentOffer != nil {
|
||||
m.releaseOffer(m.currentOffer)
|
||||
m.currentOffer = nil
|
||||
if m.currentSource != nil {
|
||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||
source.Destroy()
|
||||
}
|
||||
|
||||
if m.dataDevice != nil {
|
||||
@@ -1230,10 +1191,11 @@ func (m *Manager) clearOldEntries(days int) error {
|
||||
var toDelete [][]byte
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Skip pinned entries
|
||||
if entry.Pinned {
|
||||
continue
|
||||
}
|
||||
@@ -1348,7 +1310,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -1373,6 +1335,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
||||
continue
|
||||
}
|
||||
|
||||
entry.Data = nil
|
||||
all = append(all, entry)
|
||||
}
|
||||
return nil
|
||||
@@ -1547,7 +1510,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil || !entry.Pinned {
|
||||
continue
|
||||
}
|
||||
@@ -1565,6 +1528,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check pinned count
|
||||
cfg := m.getConfig()
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
@@ -1574,7 +1538,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
pinnedCount++
|
||||
}
|
||||
@@ -1665,11 +1629,12 @@ func (m *Manager) GetPinnedEntries() []Entry {
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.Pinned {
|
||||
entry.Data = nil
|
||||
pinned = append(pinned, entry)
|
||||
}
|
||||
}
|
||||
@@ -1695,7 +1660,7 @@ func (m *Manager) GetPinnedCount() int {
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntryMeta(v)
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
count++
|
||||
}
|
||||
@@ -1814,7 +1779,6 @@ func (m *Manager) CopyFile(filePath string) error {
|
||||
m.ownerLock.Unlock()
|
||||
})
|
||||
|
||||
m.releaseCurrentSource()
|
||||
m.currentSource = source
|
||||
|
||||
m.ownerLock.Lock()
|
||||
|
||||
@@ -2,8 +2,10 @@ package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -275,13 +277,42 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
func createPrinterViaLpadmin(name, deviceURI, ppd, information, location string) error {
|
||||
args := []string{"-p", name, "-E", "-v", deviceURI, "-m", ppd}
|
||||
if information != "" {
|
||||
args = append(args, "-D", information)
|
||||
}
|
||||
if location != "" {
|
||||
args = append(args, "-L", location)
|
||||
}
|
||||
out, err := exec.Command("lpadmin", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deletePrinterViaLpadmin(name string) error {
|
||||
out, err := exec.Command("lpadmin", "-x", name).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
|
||||
usedPkHelper := false
|
||||
|
||||
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
||||
if isAuthError(err) && m.pkHelper != nil {
|
||||
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
|
||||
return err
|
||||
// pkHelper failed (e.g., no polkit agent), try lpadmin as last resort.
|
||||
// lpadmin -E enables the printer, so no further setup needed.
|
||||
if lpadminErr := createPrinterViaLpadmin(name, deviceURI, ppd, information, location); lpadminErr != nil {
|
||||
return err
|
||||
}
|
||||
m.RefreshState()
|
||||
return nil
|
||||
}
|
||||
usedPkHelper = true
|
||||
} else if err != nil {
|
||||
@@ -308,6 +339,12 @@ func (m *Manager) DeletePrinter(printerName string) error {
|
||||
err := m.client.DeletePrinter(printerName)
|
||||
if isAuthError(err) && m.pkHelper != nil {
|
||||
err = m.pkHelper.PrinterDelete(printerName)
|
||||
if err != nil {
|
||||
// pkHelper failed, try lpadmin as last resort
|
||||
if lpadminErr := deletePrinterViaLpadmin(printerName); lpadminErr == nil {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
m.RefreshState()
|
||||
|
||||
@@ -70,6 +70,8 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
handleRestartJob(conn, req, manager)
|
||||
case "cups.holdJob":
|
||||
handleHoldJob(conn, req, manager)
|
||||
case "cups.testConnection":
|
||||
handleTestConnection(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
@@ -464,3 +466,22 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
|
||||
}
|
||||
|
||||
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
|
||||
host, err := params.StringNonEmpty(req.Params, "host")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
port := params.IntOpt(req.Params, "port", 631)
|
||||
protocol := params.StringOpt(req.Params, "protocol", "ipp")
|
||||
|
||||
result, err := manager.TestRemotePrinter(host, port, protocol)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
176
core/internal/server/cups/test_connection.go
Normal file
176
core/internal/server/cups/test_connection.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
)
|
||||
|
||||
var validProtocols = map[string]bool{
|
||||
"ipp": true,
|
||||
"ipps": true,
|
||||
"lpd": true,
|
||||
"socket": true,
|
||||
}
|
||||
|
||||
func validateTestConnectionParams(host string, port int, protocol string) error {
|
||||
if host == "" {
|
||||
return errors.New("host is required")
|
||||
}
|
||||
if strings.ContainsAny(host, " \t\n\r/\\") {
|
||||
return errors.New("host contains invalid characters")
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return errors.New("port must be between 1 and 65535")
|
||||
}
|
||||
if protocol != "" && !validProtocols[protocol] {
|
||||
return errors.New("protocol must be one of: ipp, ipps, lpd, socket")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const probeTimeout = 10 * time.Second
|
||||
|
||||
func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
// Fast fail: TCP reachability check
|
||||
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
|
||||
if err != nil {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
|
||||
}, nil
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
// Create a temporary IPP client pointing at the remote host.
|
||||
// The TCP dial above provides fast-fail for unreachable hosts.
|
||||
// The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers.
|
||||
client := ipp.NewIPPClient(host, port, "", "", useTLS)
|
||||
|
||||
// Try /ipp/print first (modern driverless printers), then / (legacy)
|
||||
info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print")
|
||||
if err != nil {
|
||||
// If we got an auth error, the printer exists but requires credentials.
|
||||
// Report it as reachable with the URI that triggered the auth challenge.
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
info, err = probeIPPEndpoint(client, host, port, useTLS, "/")
|
||||
}
|
||||
if err != nil {
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
// TCP reachable but not an IPP printer
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath)
|
||||
|
||||
httpProto := "http"
|
||||
if useTLS {
|
||||
httpProto = "https"
|
||||
}
|
||||
httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath)
|
||||
|
||||
req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1)
|
||||
req.OperationAttributes[ipp.AttributePrinterURI] = printerURI
|
||||
req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{
|
||||
ipp.AttributePrinterName,
|
||||
ipp.AttributePrinterMakeAndModel,
|
||||
ipp.AttributePrinterState,
|
||||
ipp.AttributePrinterInfo,
|
||||
ipp.AttributePrinterUriSupported,
|
||||
}
|
||||
|
||||
resp, err := client.SendRequest(httpURL, req, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.PrinterAttributes) == 0 {
|
||||
return nil, errors.New("no printer attributes returned")
|
||||
}
|
||||
|
||||
attrs := resp.PrinterAttributes[0]
|
||||
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
|
||||
Name: getStringAttr(attrs, ipp.AttributePrinterName),
|
||||
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
|
||||
State: parsePrinterState(attrs),
|
||||
URI: printerURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestRemotePrinter validates inputs and probes a remote printer via IPP.
|
||||
// For lpd/socket protocols, only TCP reachability is tested.
|
||||
func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) {
|
||||
if protocol == "" {
|
||||
protocol = "ipp"
|
||||
}
|
||||
|
||||
if err := validateTestConnectionParams(host, port, protocol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For non-IPP protocols, only check TCP reachability
|
||||
if protocol == "lpd" || protocol == "socket" {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
|
||||
if err != nil {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
|
||||
}, nil
|
||||
}
|
||||
conn.Close()
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d", protocol, host, port),
|
||||
}, nil
|
||||
}
|
||||
|
||||
useTLS := protocol == "ipps"
|
||||
|
||||
probeFn := m.probeRemoteFn
|
||||
if probeFn == nil {
|
||||
probeFn = probeRemotePrinter
|
||||
}
|
||||
|
||||
return probeFn(host, port, useTLS)
|
||||
}
|
||||
397
core/internal/server/cups/test_connection_test.go
Normal file
397
core/internal/server/cups/test_connection_test.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateTestConnectionParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
protocol string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid ipp",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid ipps",
|
||||
host: "printer.local",
|
||||
port: 443,
|
||||
protocol: "ipps",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid lpd",
|
||||
host: "10.0.0.1",
|
||||
port: 515,
|
||||
protocol: "lpd",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid socket",
|
||||
host: "10.0.0.1",
|
||||
port: 9100,
|
||||
protocol: "socket",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "empty host",
|
||||
host: "",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host is required",
|
||||
},
|
||||
{
|
||||
name: "port too low",
|
||||
host: "192.168.0.5",
|
||||
port: 0,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "port too high",
|
||||
host: "192.168.0.5",
|
||||
port: 70000,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ftp",
|
||||
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
|
||||
},
|
||||
{
|
||||
name: "empty protocol treated as ipp",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "host with slash",
|
||||
host: "192.168.0.5/admin",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
{
|
||||
name: "host with space",
|
||||
host: "192.168.0.5 ",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
{
|
||||
name: "host with newline",
|
||||
host: "192.168.0.5\n",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateTestConnectionParams(tt.host, tt.port, tt.protocol)
|
||||
if tt.wantErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_Validation(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
protocol string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty host returns error",
|
||||
host: "",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host is required",
|
||||
},
|
||||
{
|
||||
name: "invalid port returns error",
|
||||
host: "192.168.0.5",
|
||||
port: 0,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol returns error",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ftp",
|
||||
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := m.TestRemotePrinter(tt.host, tt.port, tt.protocol)
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_IPP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
probeRet *RemotePrinterInfo
|
||||
probeErr error
|
||||
wantTLS bool
|
||||
wantReach bool
|
||||
wantModel string
|
||||
}{
|
||||
{
|
||||
name: "successful ipp probe",
|
||||
protocol: "ipp",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
Name: "OfficeJet",
|
||||
State: "idle",
|
||||
URI: "ipp://192.168.0.5:631/ipp/print",
|
||||
},
|
||||
wantTLS: false,
|
||||
wantReach: true,
|
||||
wantModel: "HP OfficeJet 8010",
|
||||
},
|
||||
{
|
||||
name: "successful ipps probe",
|
||||
protocol: "ipps",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
URI: "ipps://192.168.0.5:631/ipp/print",
|
||||
},
|
||||
wantTLS: true,
|
||||
wantReach: true,
|
||||
wantModel: "HP OfficeJet 8010",
|
||||
},
|
||||
{
|
||||
name: "unreachable host",
|
||||
protocol: "ipp",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: "cannot reach 192.168.0.5:631: connection refused",
|
||||
},
|
||||
wantReach: false,
|
||||
},
|
||||
{
|
||||
name: "empty protocol defaults to ipp",
|
||||
protocol: "",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "Test Printer",
|
||||
},
|
||||
wantTLS: false,
|
||||
wantReach: true,
|
||||
wantModel: "Test Printer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedTLS bool
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
capturedTLS = useTLS
|
||||
return tt.probeRet, tt.probeErr
|
||||
}
|
||||
|
||||
result, err := m.TestRemotePrinter("192.168.0.5", 631, tt.protocol)
|
||||
if tt.probeErr != nil {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantReach, result.Reachable)
|
||||
assert.Equal(t, tt.wantModel, result.MakeModel)
|
||||
assert.Equal(t, tt.wantTLS, capturedTLS)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_AuthRequired(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
// Simulate what happens when the printer returns HTTP 401
|
||||
return probeRemotePrinterWithAuthError(host, port, useTLS)
|
||||
}
|
||||
|
||||
result, err := m.TestRemotePrinter("192.168.0.107", 631, "ipp")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Reachable)
|
||||
assert.Equal(t, "authentication required", result.Info)
|
||||
assert.Contains(t, result.URI, "ipp://192.168.0.107:631")
|
||||
}
|
||||
|
||||
// probeRemotePrinterWithAuthError simulates a probe where the printer
|
||||
// returns HTTP 401 on both endpoints.
|
||||
func probeRemotePrinterWithAuthError(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
// This simulates what probeRemotePrinter does when both endpoints
|
||||
// return auth errors. We test the auth detection logic directly.
|
||||
err := ipp.HTTPError{Code: 401}
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_NonIPPProtocol(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
probeCalled := false
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
probeCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// These will fail at TCP dial (no real server), but the important
|
||||
// thing is that probeRemoteFn is NOT called for lpd/socket.
|
||||
m.TestRemotePrinter("192.168.0.5", 9100, "socket")
|
||||
assert.False(t, probeCalled, "probe function should not be called for socket protocol")
|
||||
|
||||
m.TestRemotePrinter("192.168.0.5", 515, "lpd")
|
||||
assert.False(t, probeCalled, "probe function should not be called for lpd protocol")
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_Success(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
Name: "OfficeJet",
|
||||
State: "idle",
|
||||
URI: "ipp://192.168.0.5:631/ipp/print",
|
||||
}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{
|
||||
"host": "192.168.0.5",
|
||||
},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
assert.Equal(t, "HP OfficeJet 8010", resp.Result.MakeModel)
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_MissingHost(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, resp.Result)
|
||||
assert.NotNil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
assert.Equal(t, 9631, port)
|
||||
assert.True(t, useTLS)
|
||||
return &RemotePrinterInfo{Reachable: true, URI: "ipps://192.168.0.5:9631/ipp/print"}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{
|
||||
"host": "192.168.0.5",
|
||||
"port": float64(9631),
|
||||
"protocol": "ipps",
|
||||
},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
}
|
||||
|
||||
func TestHandleRequest_TestConnection(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
return &RemotePrinterInfo{Reachable: true}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{"host": "192.168.0.5"},
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
}
|
||||
@@ -55,6 +55,16 @@ type PPD struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type RemotePrinterInfo struct {
|
||||
Reachable bool `json:"reachable"`
|
||||
MakeModel string `json:"makeModel"`
|
||||
Name string `json:"name"`
|
||||
Info string `json:"info"`
|
||||
State string `json:"state"`
|
||||
URI string `json:"uri"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type PrinterClass struct {
|
||||
Name string `json:"name"`
|
||||
URI string `json:"uri"`
|
||||
@@ -77,6 +87,7 @@ type Manager struct {
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotifiedState *CUPSState
|
||||
baseURL string
|
||||
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
|
||||
}
|
||||
|
||||
type SubscriptionManagerInterface interface {
|
||||
|
||||
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
|
||||
|
||||
func InitializeManager() (*Manager, error) {
|
||||
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
||||
return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
|
||||
return nil, fmt.Errorf("insufficient permissions to access input devices")
|
||||
}
|
||||
|
||||
return NewManager()
|
||||
|
||||
@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
|
||||
return false
|
||||
}
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
log.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", name)
|
||||
log.Warnf("Screensaver name %s already owned by another process", name)
|
||||
return false
|
||||
}
|
||||
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
||||
|
||||
61
core/internal/server/location/handlers.go
Normal file
61
core/internal/server/location/handlers.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
type LocationEvent struct {
|
||||
Type string `json:"type"`
|
||||
Data State `json:"data"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "location.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "location.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
event := LocationEvent{
|
||||
Type: "state_changed",
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
event := LocationEvent{
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
175
core/internal/server/location/manager.go
Normal file
175
core/internal/server/location/manager.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func NewManager(client geolocation.Client) (*Manager, error) {
|
||||
currLocation, err := client.GetLocation()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial location: %v", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
client: client,
|
||||
dirty: make(chan struct{}),
|
||||
stopChan: make(chan struct{}),
|
||||
|
||||
state: &State{
|
||||
Latitude: currLocation.Latitude,
|
||||
Longitude: currLocation.Longitude,
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.startSignalPump(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.notifierWg.Add(1)
|
||||
go m.notifier()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
m.notifierWg.Wait()
|
||||
|
||||
m.sigWG.Wait()
|
||||
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
close(ch)
|
||||
m.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan State {
|
||||
ch := make(chan State, 64)
|
||||
m.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) startSignalPump() error {
|
||||
m.sigWG.Add(1)
|
||||
go func() {
|
||||
defer m.sigWG.Done()
|
||||
|
||||
subscription := m.client.Subscribe("locationManager")
|
||||
defer m.client.Unsubscribe("locationManager")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case location, ok := <-subscription:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
m.handleLocationChange(location)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleLocationChange(location geolocation.Location) {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Latitude = location.Latitude
|
||||
m.state.Longitude = location.Longitude
|
||||
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
if m.state == nil {
|
||||
return State{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
}
|
||||
}
|
||||
stateCopy := *m.state
|
||||
return stateCopy
|
||||
}
|
||||
|
||||
func (m *Manager) notifier() {
|
||||
defer m.notifierWg.Done()
|
||||
const minGap = 200 * time.Millisecond
|
||||
timer := time.NewTimer(minGap)
|
||||
timer.Stop()
|
||||
var pending bool
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-m.dirty:
|
||||
if pending {
|
||||
continue
|
||||
}
|
||||
pending = true
|
||||
timer.Reset(minGap)
|
||||
case <-timer.C:
|
||||
if !pending {
|
||||
continue
|
||||
}
|
||||
|
||||
currentState := m.GetState()
|
||||
|
||||
if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) {
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
select {
|
||||
case ch <- currentState:
|
||||
default:
|
||||
log.Warn("Location: subscriber channel full, dropping update")
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
stateCopy := currentState
|
||||
m.lastNotified = &stateCopy
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stateChanged(old, new *State) bool {
|
||||
if old == nil || new == nil {
|
||||
return true
|
||||
}
|
||||
if old.Latitude != new.Latitude {
|
||||
return true
|
||||
}
|
||||
if old.Longitude != new.Longitude {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
28
core/internal/server/location/types.go
Normal file
28
core/internal/server/location/types.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *State
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
client geolocation.Client
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
|
||||
subscribers syncmap.Map[string, chan State]
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotified *State
|
||||
}
|
||||
@@ -29,6 +29,7 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
|
||||
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||
Contrast: models.GetOr(req, "contrast", 0.0),
|
||||
}
|
||||
|
||||
wait := models.GetOr(req, "wait", true)
|
||||
|
||||
@@ -10,6 +10,7 @@ type Backend interface {
|
||||
ScanWiFi() error
|
||||
ScanWiFiDevice(device string) error
|
||||
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
|
||||
GetWiFiQRCodeContent(ssid string) (string, error)
|
||||
GetWiFiDevices() []WiFiDevice
|
||||
|
||||
ConnectWiFi(req ConnectionRequest) error
|
||||
|
||||
@@ -111,6 +111,10 @@ func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkI
|
||||
return b.wifi.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
return b.wifi.GetWiFiQRCodeContent(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
if err := b.wifi.ConnectWiFi(req); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package network
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
||||
@@ -112,3 +115,19 @@ func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
|
||||
Networks: b.state.WiFiNetworks,
|
||||
}}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
path := iwdConfigPath(ssid)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no saved iwd config for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
passphrase, err := parseIWDPassphrase(string(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read passphrase for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
return FormatWiFiQRString("WPA", ssid, passphrase), nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInf
|
||||
return nil, fmt.Errorf("WiFi details not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
return "", fmt.Errorf("WiFi QR Code not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
return fmt.Errorf("WiFi connect not supported by networkd backend")
|
||||
}
|
||||
|
||||
@@ -158,26 +158,18 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
||||
|
||||
channel := frequencyToChannel(freq)
|
||||
|
||||
isConnected := ssid == currentSSID && bssid == currentBSSID
|
||||
rate := maxBitrate / 1000
|
||||
if isConnected {
|
||||
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||
rate = devBitrate / 1000
|
||||
}
|
||||
}
|
||||
|
||||
network := WiFiNetwork{
|
||||
SSID: ssid,
|
||||
BSSID: bssid,
|
||||
Signal: strength,
|
||||
Secured: secured,
|
||||
Enterprise: enterprise,
|
||||
Connected: isConnected,
|
||||
Connected: ssid == currentSSID && bssid == currentBSSID,
|
||||
Saved: savedSSIDs[ssid],
|
||||
Autoconnect: autoconnectMap[ssid],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: rate,
|
||||
Rate: maxBitrate / 1000,
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
@@ -204,6 +196,65 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
conn, err := b.findConnection(ssid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no saved connection for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get settings for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
secSettings, ok := connSettings["802-11-wireless-security"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("network `%s` has no security settings", ssid)
|
||||
}
|
||||
|
||||
keyMgmt, ok := secSettings["key-mgmt"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
|
||||
}
|
||||
|
||||
var securityType string
|
||||
switch keyMgmt {
|
||||
case "none":
|
||||
authAlg, _ := secSettings["auth-alg"].(string)
|
||||
switch authAlg {
|
||||
case "open":
|
||||
securityType = "nopass"
|
||||
default:
|
||||
securityType = "WEP"
|
||||
}
|
||||
case "ieee8021x":
|
||||
securityType = "WEP"
|
||||
default:
|
||||
securityType = "WPA"
|
||||
}
|
||||
|
||||
if securityType != "WPA" {
|
||||
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
|
||||
}
|
||||
|
||||
secrets, err := conn.GetSecrets("802-11-wireless-security")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve connection secrets for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
secSecrets, ok := secrets["802-11-wireless-security"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
|
||||
}
|
||||
|
||||
psk, ok := secSecrets["psk"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
|
||||
}
|
||||
|
||||
return FormatWiFiQRString(securityType, ssid, psk), nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
devInfo, err := b.getWifiDeviceForConnection(req.Device)
|
||||
if err != nil {
|
||||
@@ -463,27 +514,19 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
||||
|
||||
channel := frequencyToChannel(freq)
|
||||
|
||||
isConnected := ssid == currentSSID
|
||||
rate := maxBitrate / 1000
|
||||
if isConnected {
|
||||
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||
rate = devBitrate / 1000
|
||||
}
|
||||
}
|
||||
|
||||
network := WiFiNetwork{
|
||||
SSID: ssid,
|
||||
BSSID: bssid,
|
||||
Signal: strength,
|
||||
Secured: secured,
|
||||
Enterprise: enterprise,
|
||||
Connected: isConnected,
|
||||
Connected: ssid == currentSSID,
|
||||
Saved: savedSSIDs[ssid],
|
||||
Autoconnect: autoconnectMap[ssid],
|
||||
Hidden: hiddenSSIDs[ssid],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: rate,
|
||||
Rate: maxBitrate / 1000,
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
@@ -1019,27 +1062,19 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
||||
|
||||
channel := frequencyToChannel(freq)
|
||||
|
||||
isConnected := connected && apSSID == ssid
|
||||
rate := maxBitrate / 1000
|
||||
if isConnected {
|
||||
if devBitrate, err := devInfo.wireless.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||
rate = devBitrate / 1000
|
||||
}
|
||||
}
|
||||
|
||||
network := WiFiNetwork{
|
||||
SSID: apSSID,
|
||||
BSSID: apBSSID,
|
||||
Signal: strength,
|
||||
Secured: secured,
|
||||
Enterprise: enterprise,
|
||||
Connected: isConnected,
|
||||
Connected: connected && apSSID == ssid,
|
||||
Saved: savedSSIDs[apSSID],
|
||||
Autoconnect: autoconnectMap[apSSID],
|
||||
Hidden: hiddenSSIDs[apSSID],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: rate,
|
||||
Rate: maxBitrate / 1000,
|
||||
Channel: channel,
|
||||
Device: name,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
@@ -40,6 +41,10 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
handleSetPreference(conn, req, manager)
|
||||
case "network.info":
|
||||
handleGetNetworkInfo(conn, req, manager)
|
||||
case "network.qrcode":
|
||||
handleGetNetworkQRCode(conn, req, manager)
|
||||
case "network.delete-qrcode":
|
||||
handleDeleteQRCode(conn, req, manager)
|
||||
case "network.ethernet.info":
|
||||
handleGetWiredNetworkInfo(conn, req, manager)
|
||||
case "network.subscribe":
|
||||
@@ -320,6 +325,42 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, network)
|
||||
}
|
||||
|
||||
func handleGetNetworkQRCode(conn net.Conn, req models.Request, manager *Manager) {
|
||||
ssid, err := params.String(req.Params, "ssid")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
content, err := manager.GetNetworkQRCode(ssid)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, content)
|
||||
}
|
||||
|
||||
func handleDeleteQRCode(conn net.Conn, req models.Request, _ *Manager) {
|
||||
path, err := params.String(req.Params, "path")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidQRCodePath(path) {
|
||||
models.RespondError(conn, req.ID, "invalid QR code path")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "QR code file deleted"})
|
||||
}
|
||||
|
||||
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
uuid, err := params.String(req.Params, "uuid")
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
@@ -438,6 +440,43 @@ func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, err
|
||||
return m.backend.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (m *Manager) GetNetworkQRCode(ssid string) ([2]string, error) {
|
||||
content, err := m.backend.GetWiFiQRCodeContent(ssid)
|
||||
if err != nil {
|
||||
return [2]string{}, err
|
||||
}
|
||||
|
||||
qrc, err := qrcode.New(content)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
pathThemed, pathNormal := qrCodePaths(ssid)
|
||||
|
||||
wThemed, err := standard.New(
|
||||
pathThemed,
|
||||
standard.WithBuiltinImageEncoder(standard.PNG_FORMAT),
|
||||
standard.WithBgTransparent(),
|
||||
standard.WithFgColorRGBHex("#ffffff"),
|
||||
)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(wThemed); err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
wNormal, err := standard.New(pathNormal, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(wNormal); err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
return [2]string{pathThemed, pathNormal}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ToggleWiFi() error {
|
||||
enabled, err := m.backend.GetWiFiEnabled()
|
||||
if err != nil {
|
||||
|
||||
59
core/internal/server/network/wifi_qrcode.go
Normal file
59
core/internal/server/network/wifi_qrcode.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const qrCodeTmpPrefix = "/tmp/dank-wifi-qrcode-"
|
||||
|
||||
func FormatWiFiQRString(securityType, ssid, password string) string {
|
||||
return fmt.Sprintf("WIFI:T:%s;S:%s;P:%s;;", securityType, ssid, password)
|
||||
}
|
||||
|
||||
func qrCodePaths(ssid string) (themed, normal string) {
|
||||
safe := sanitizeSSIDForPath(ssid)
|
||||
themed = fmt.Sprintf("%s%s-themed.png", qrCodeTmpPrefix, safe)
|
||||
normal = fmt.Sprintf("%s%s-normal.png", qrCodeTmpPrefix, safe)
|
||||
return
|
||||
}
|
||||
|
||||
func isValidQRCodePath(path string) bool {
|
||||
clean := filepath.Clean(path)
|
||||
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png")
|
||||
}
|
||||
|
||||
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
|
||||
func sanitizeSSIDForPath(ssid string) string {
|
||||
return safePathChar.ReplaceAllString(ssid, "_")
|
||||
}
|
||||
|
||||
var iwdVerbatimSSID = regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`)
|
||||
|
||||
func iwdConfigPath(ssid string) string {
|
||||
switch {
|
||||
case iwdVerbatimSSID.MatchString(ssid):
|
||||
return fmt.Sprintf("/var/lib/iwd/%s.psk", ssid)
|
||||
default:
|
||||
return fmt.Sprintf("/var/lib/iwd/=%x.psk", []byte(ssid))
|
||||
}
|
||||
}
|
||||
|
||||
func parseIWDPassphrase(data string) (string, error) {
|
||||
inSecurity := false
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case line == "[Security]":
|
||||
inSecurity = true
|
||||
case strings.HasPrefix(line, "["):
|
||||
inSecurity = false
|
||||
case inSecurity && strings.HasPrefix(line, "Passphrase="):
|
||||
return strings.TrimPrefix(line, "Passphrase="), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no passphrase found in iwd config")
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
@@ -192,6 +193,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "location.") {
|
||||
if locationManager == nil {
|
||||
models.RespondError(conn, req.ID, "location manager not initialized")
|
||||
return
|
||||
}
|
||||
location.HandleRequest(conn, req, locationManager)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "ping":
|
||||
models.Respond(conn, req.ID, "pong")
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
@@ -70,6 +72,8 @@ var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
var locationManager *location.Manager
|
||||
var geoClientInstance geolocation.Client
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
@@ -390,6 +394,19 @@ func InitializeThemeModeManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeLocationManager(geoClient geolocation.Client) error {
|
||||
manager, err := location.NewManager(geoClient)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize location manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
locationManager = manager
|
||||
|
||||
log.Info("Location manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -537,6 +554,10 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if locationManager != nil {
|
||||
caps = append(caps, "location")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
@@ -1307,6 +1328,12 @@ func cleanupManagers() {
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
if locationManager != nil {
|
||||
locationManager.Close()
|
||||
}
|
||||
if geoClientInstance != nil {
|
||||
geoClientInstance.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Start(printDocs bool) error {
|
||||
@@ -1488,6 +1515,9 @@ func Start(printDocs bool) error {
|
||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
||||
log.Info("Location:")
|
||||
log.Info(" location.getState - Get current location state")
|
||||
log.Info(" location.subscribe - Subscribe to location changes (streaming)")
|
||||
log.Info("")
|
||||
}
|
||||
log.Info("Initializing managers...")
|
||||
@@ -1567,6 +1597,37 @@ func Start(printDocs bool) error {
|
||||
log.Warnf("Wayland manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := InitializeThemeModeManager(); err != nil {
|
||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
if loginctlManager == nil {
|
||||
return
|
||||
}
|
||||
themeModeManager.WatchLoginctl(loginctlManager)
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
geoClient := geolocation.NewClient()
|
||||
geoClientInstance = geoClient
|
||||
|
||||
if waylandManager != nil {
|
||||
waylandManager.SetGeoClient(geoClient)
|
||||
}
|
||||
if themeModeManager != nil {
|
||||
themeModeManager.SetGeoClient(geoClient)
|
||||
}
|
||||
|
||||
if err := InitializeLocationManager(geoClient); err != nil {
|
||||
log.Warnf("Location manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := InitializeBluezManager(); err != nil {
|
||||
log.Warnf("Bluez manager unavailable: %v", err)
|
||||
@@ -1595,19 +1656,6 @@ func Start(printDocs bool) error {
|
||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := InitializeThemeModeManager(); err != nil {
|
||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
if loginctlManager == nil {
|
||||
return
|
||||
}
|
||||
themeModeManager.WatchLoginctl(loginctlManager)
|
||||
}()
|
||||
}
|
||||
|
||||
fatalErrChan := make(chan error, 1)
|
||||
if wlrOutputManager != nil {
|
||||
go func() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
@@ -32,6 +33,8 @@ type Manager struct {
|
||||
cachedIPLat *float64
|
||||
cachedIPLon *float64
|
||||
|
||||
geoClient geolocation.Client
|
||||
|
||||
stopChan chan struct{}
|
||||
updateTrigger chan struct{}
|
||||
wg sync.WaitGroup
|
||||
@@ -311,6 +314,10 @@ func (m *Manager) getConfig() Config {
|
||||
return m.config
|
||||
}
|
||||
|
||||
func (m *Manager) SetGeoClient(client geolocation.Client) {
|
||||
m.geoClient = client
|
||||
}
|
||||
|
||||
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
if config.Latitude != nil && config.Longitude != nil {
|
||||
return config.Latitude, config.Longitude
|
||||
@@ -318,6 +325,9 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
if !config.UseIPLocation {
|
||||
return nil, nil
|
||||
}
|
||||
if m.geoClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
@@ -327,17 +337,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
lat, lon, err := wayland.FetchIPLocation()
|
||||
location, err := m.geoClient.GetLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.locationMutex.Unlock()
|
||||
|
||||
return lat, lon
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
}
|
||||
|
||||
func statesEqual(a, b *State) bool {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||
)
|
||||
@@ -420,6 +421,10 @@ func (m *Manager) recalcSchedule(now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetGeoClient(client geolocation.Client) {
|
||||
m.geoClient = client
|
||||
}
|
||||
|
||||
func (m *Manager) getLocation() (*float64, *float64) {
|
||||
m.configMutex.RLock()
|
||||
config := m.config
|
||||
@@ -428,26 +433,31 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
||||
if config.Latitude != nil && config.Longitude != nil {
|
||||
return config.Latitude, config.Longitude
|
||||
}
|
||||
if config.UseIPLocation {
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||
m.locationMutex.RUnlock()
|
||||
return lat, lon
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
if !config.UseIPLocation {
|
||||
return nil, nil
|
||||
}
|
||||
if m.geoClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
lat, lon, err := FetchIPLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.locationMutex.Unlock()
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||
m.locationMutex.RUnlock()
|
||||
return lat, lon
|
||||
}
|
||||
return nil, nil
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
location, err := m.geoClient.GetLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.locationMutex.Unlock()
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
}
|
||||
|
||||
func (m *Manager) hasValidSchedule() bool {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -97,6 +98,8 @@ type Manager struct {
|
||||
dbusConn *dbus.Conn
|
||||
dbusSignal chan *dbus.Signal
|
||||
|
||||
geoClient geolocation.Client
|
||||
|
||||
lastAppliedTemp int
|
||||
lastAppliedGamma float64
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package tui
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -43,9 +42,6 @@ type Model struct {
|
||||
sudoPassword string
|
||||
existingConfigs []ExistingConfigInfo
|
||||
fingerprintFailed bool
|
||||
|
||||
availablePrivesc []privesc.Tool
|
||||
selectedPrivesc int
|
||||
}
|
||||
|
||||
func NewModel(version string, logFilePath string) Model {
|
||||
@@ -151,8 +147,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.updateGentooUseFlagsState(msg)
|
||||
case StateGentooGCCCheck:
|
||||
return m.updateGentooGCCCheckState(msg)
|
||||
case StateSelectPrivesc:
|
||||
return m.updateSelectPrivescState(msg)
|
||||
case StateAuthMethodChoice:
|
||||
return m.updateAuthMethodChoiceState(msg)
|
||||
case StateFingerprintAuth:
|
||||
@@ -195,8 +189,6 @@ func (m Model) View() string {
|
||||
return m.viewGentooUseFlags()
|
||||
case StateGentooGCCCheck:
|
||||
return m.viewGentooGCCCheck()
|
||||
case StateSelectPrivesc:
|
||||
return m.viewSelectPrivesc()
|
||||
case StateAuthMethodChoice:
|
||||
return m.viewAuthMethodChoice()
|
||||
case StateFingerprintAuth:
|
||||
|
||||
@@ -10,7 +10,6 @@ const (
|
||||
StateDependencyReview
|
||||
StateGentooUseFlags
|
||||
StateGentooGCCCheck
|
||||
StateSelectPrivesc
|
||||
StateAuthMethodChoice
|
||||
StateFingerprintAuth
|
||||
StatePasswordPrompt
|
||||
|
||||
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
|
||||
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render("Setting up configuration files...")
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Show progress information
|
||||
|
||||
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
|
||||
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -180,7 +180,16 @@ func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return m.enterAuthPhase()
|
||||
// Check if fingerprint is enabled
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0 // Default to fingerprint
|
||||
return m, nil
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
return m, nil
|
||||
}
|
||||
case "esc":
|
||||
m.state = StateSelectWindowManager
|
||||
return m, nil
|
||||
|
||||
@@ -56,7 +56,14 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.state = StateGentooGCCCheck
|
||||
return m, nil
|
||||
}
|
||||
return m.enterAuthPhase()
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
@@ -68,7 +75,14 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.selectedWM == 1 {
|
||||
return m, m.checkGCCVersion()
|
||||
}
|
||||
return m.enterAuthPhase()
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
}
|
||||
return m, nil
|
||||
case "esc":
|
||||
m.state = StateDependencyReview
|
||||
return m, nil
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
|
||||
if !m.packageProgress.isComplete {
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render(m.packageProgress.step)
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Show progress bar
|
||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
||||
if isGit {
|
||||
return "dms-shell-git"
|
||||
}
|
||||
return "dms-shell"
|
||||
return "dms-shell-bin"
|
||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||
if isGit {
|
||||
return "dms-git"
|
||||
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
|
||||
|
||||
for i := startIdx; i < len(allLogs); i++ {
|
||||
if allLogs[i] != "" {
|
||||
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
|
||||
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
@@ -76,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
|
||||
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render("Waiting for fingerprint...")
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
@@ -275,7 +274,8 @@ func (m Model) delayThenReturn() tea.Cmd {
|
||||
|
||||
func (m Model) tryFingerprint() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
_ = privesc.ClearCache(context.Background())
|
||||
clearCmd := exec.Command("sudo", "-k")
|
||||
clearCmd.Run()
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||
@@ -289,9 +289,15 @@ func (m Model) tryFingerprint() tea.Cmd {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := privesc.ValidateWithAskpass(ctx, askpassScript); err != nil {
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: "", valid: true}
|
||||
}
|
||||
}
|
||||
@@ -301,9 +307,32 @@ func (m Model) validatePassword(password string) tea.Cmd {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := privesc.ValidatePassword(ctx, password); err != nil {
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(stdin, "%s\n", password)
|
||||
stdin.Close()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) viewSelectPrivesc() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.styles.Title.Render("Privilege Escalation Tool"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.styles.Normal.Render("Multiple privilege tools are available. Choose one for installation:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for i, t := range m.availablePrivesc {
|
||||
label := fmt.Sprintf("%s — %s", t.Name(), privescToolDescription(t))
|
||||
switch i {
|
||||
case m.selectedPrivesc:
|
||||
b.WriteString(m.styles.SelectedOption.Render("▶ " + label))
|
||||
default:
|
||||
b.WriteString(m.styles.Normal.Render(" " + label))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Set %s=<tool> to skip this prompt in future runs.", privesc.EnvVar)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateSelectPrivescState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
keyMsg, ok := msg.(tea.KeyMsg)
|
||||
if !ok {
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
if m.selectedPrivesc > 0 {
|
||||
m.selectedPrivesc--
|
||||
}
|
||||
case "down":
|
||||
if m.selectedPrivesc < len(m.availablePrivesc)-1 {
|
||||
m.selectedPrivesc++
|
||||
}
|
||||
case "enter":
|
||||
chosen := m.availablePrivesc[m.selectedPrivesc]
|
||||
if err := privesc.SetTool(chosen); err != nil {
|
||||
m.err = fmt.Errorf("failed to select %s: %w", chosen.Name(), err)
|
||||
m.state = StateError
|
||||
return m, nil
|
||||
}
|
||||
return m.routeToAuthAfterPrivesc()
|
||||
case "esc":
|
||||
m.state = StateDependencyReview
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func privescToolDescription(t privesc.Tool) string {
|
||||
switch t {
|
||||
case privesc.ToolSudo:
|
||||
return "classic sudo (supports password prompt in this installer)"
|
||||
case privesc.ToolDoas:
|
||||
return "OpenBSD-style doas (requires persist or nopass in /etc/doas.conf)"
|
||||
case privesc.ToolRun0:
|
||||
return "systemd run0 (authenticated via polkit)"
|
||||
default:
|
||||
return string(t)
|
||||
}
|
||||
}
|
||||
|
||||
// routeToAuthAfterPrivesc advances from the privesc-selection screen to the
|
||||
// right auth flow. Sudo goes through the fingerprint/password path; doas and
|
||||
// run0 skip password entry and proceed to install.
|
||||
func (m Model) routeToAuthAfterPrivesc() (tea.Model, tea.Cmd) {
|
||||
tool, err := privesc.Detect()
|
||||
if err != nil {
|
||||
m.err = err
|
||||
m.state = StateError
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if tool == privesc.ToolSudo {
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0
|
||||
return m, nil
|
||||
}
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.sudoPassword = ""
|
||||
m.packageProgress = packageInstallProgressMsg{}
|
||||
m.state = StateInstallingPackages
|
||||
m.isLoading = true
|
||||
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
||||
}
|
||||
|
||||
// enterAuthPhase is called when dependency review (or the Gentoo screens)
|
||||
// finish. It either routes directly to the sudo/fingerprint flow or shows
|
||||
// the privesc-tool selection screen when multiple tools are available and
|
||||
// no $DMS_PRIVESC override is set.
|
||||
func (m Model) enterAuthPhase() (tea.Model, tea.Cmd) {
|
||||
tools := privesc.AvailableTools()
|
||||
_, envSet := privesc.EnvOverride()
|
||||
|
||||
if len(tools) == 0 {
|
||||
m.err = fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||
m.state = StateError
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if envSet || len(tools) == 1 {
|
||||
return m.routeToAuthAfterPrivesc()
|
||||
}
|
||||
|
||||
m.availablePrivesc = tools
|
||||
m.selectedPrivesc = 0
|
||||
m.state = StateSelectPrivesc
|
||||
return m, nil
|
||||
}
|
||||
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
|
||||
contentStyle = contentStyle.Bold(true)
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
||||
fmt.Fprintf(&b, " %s %s\n",
|
||||
prefixStyle.Render(prefix),
|
||||
contentStyle.Render(content)))
|
||||
contentStyle.Render(content))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
|
||||
} else if m.isLoading {
|
||||
spinner := m.spinner.View()
|
||||
loading := m.styles.Normal.Render("Detecting system...")
|
||||
b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading))
|
||||
fmt.Fprintf(&b, "%s %s\n\n", spinner, loading)
|
||||
}
|
||||
|
||||
// Footer with better visual separation
|
||||
|
||||
@@ -27,12 +27,12 @@ override_dh_auto_build:
|
||||
# Verify core directory exists (native package format has source at root)
|
||||
test -d core || (echo "ERROR: core directory not found!" && exit 1)
|
||||
|
||||
# Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
|
||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
|
||||
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
||||
GO_INSTALLED=$$(go version | grep -oP 'go\K[0-9]+\.[0-9]+'); \
|
||||
sed -i "s/^go [0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/go $${GO_INSTALLED}/" core/go.mod; \
|
||||
sed -i "s/^\(## explicit; go \)[0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/\1$${GO_INSTALLED}/" core/vendor/modules.txt
|
||||
|
||||
# Build dms-cli from source using vendored dependencies
|
||||
# Extract version info and build in single shell to preserve variables
|
||||
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
|
||||
# Build dms-cli (single shell to preserve variables; arch: Debian amd64/arm64 -> Makefile amd64/arm64)
|
||||
VERSION="$(UPSTREAM_VERSION)"; \
|
||||
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \
|
||||
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user