mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-04 19:42:08 -04:00
Compare commits
323 Commits
hotfix-1.4
...
blur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6416c8ba | ||
|
|
a0b2debd7e | ||
|
|
c471cff456 | ||
|
|
f83bb10e0c | ||
|
|
74ad58b1e1 | ||
|
|
577863b969 | ||
|
|
03d2a3fd39 | ||
|
|
802b23ed60 | ||
|
|
2b9f3a9eef | ||
|
|
62c60900eb | ||
|
|
b381e1e54c | ||
|
|
e7ee26ce74 | ||
|
|
521a3fa6e8 | ||
|
|
5ee93a67fe | ||
|
|
5d0a03c822 | ||
|
|
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 }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y 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
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
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:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/prek.yml
vendored
4
.github/workflows/prek.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y 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
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v6
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
# fetch-depth: 0
|
# fetch-depth: 0
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -192,12 +192,12 @@ jobs:
|
|||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
4
.github/workflows/run-copr.yml
vendored
4
.github/workflows/run-copr.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
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
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-git
|
|
||||||
- dms-greeter
|
- dms-greeter
|
||||||
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -119,9 +119,8 @@ jobs:
|
|||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
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=()
|
PACKAGES_TO_UPDATE=()
|
||||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
|
||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
PACKAGES_TO_UPDATE+=("dms")
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
@@ -140,7 +139,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "✓ All packages up to date"
|
echo "✓ Both packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -196,10 +195,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Wait before OBS upload
|
||||||
|
run: sleep 3
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
@@ -245,7 +247,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
- 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: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
@@ -266,7 +268,7 @@ jobs:
|
|||||||
} > distro/opensuse/dms-git.spec
|
} > distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
- name: Update Debian dms-git changelog version
|
- 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: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
@@ -345,7 +347,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -389,7 +391,7 @@ jobs:
|
|||||||
UPLOADED_PACKAGES=()
|
UPLOADED_PACKAGES=()
|
||||||
SKIPPED_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
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
28
.github/workflows/run-ppa.yml
vendored
28
.github/workflows/run-ppa.yml
vendored
@@ -4,9 +4,15 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
description: "Package to upload"
|
||||||
required: false
|
required: true
|
||||||
default: "dms-git"
|
type: choice
|
||||||
|
options:
|
||||||
|
- dms
|
||||||
|
- dms-greeter
|
||||||
|
- dms-git
|
||||||
|
- all
|
||||||
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||||
required: false
|
required: false
|
||||||
@@ -25,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -139,7 +145,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Fallback
|
# Fallback
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -151,12 +157,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
@@ -209,7 +215,7 @@ jobs:
|
|||||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
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
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
# Map package to PPA name
|
# Map package to PPA name
|
||||||
@@ -236,7 +242,11 @@ jobs:
|
|||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
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
|
done
|
||||||
|
|
||||||
- name: Summary
|
- 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 }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
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.
|
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
|
# 1.4.0
|
||||||
|
|
||||||
- Overhauled system monitor, graphs, styling
|
- Overhauled system monitor, graphs, styling
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
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
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
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
|
all: build
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ clean:
|
|||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
lint-qml:
|
||||||
|
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -76,7 +79,7 @@ install-desktop:
|
|||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entry installed"
|
@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 ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -130,6 +133,7 @@ help:
|
|||||||
@echo " all (default) - Build the DMS binary"
|
@echo " all (default) - Build the DMS binary"
|
||||||
@echo " build - Same as 'all'"
|
@echo " build - Same as 'all'"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ packages:
|
|||||||
outpkg: mocks_brightness
|
outpkg: mocks_brightness
|
||||||
interfaces:
|
interfaces:
|
||||||
DBusConn:
|
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:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/network"
|
dir: "internal/mocks/network"
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
repos:
|
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
|
- repo: local
|
||||||
hooks:
|
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
|
- id: go-test
|
||||||
name: go test
|
name: go test
|
||||||
entry: go test ./...
|
entry: go test ./...
|
||||||
|
|||||||
@@ -63,19 +63,19 @@ endif
|
|||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
install: build
|
install:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-all: build-all
|
install-all:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-dankinstall: dankinstall
|
install-dankinstall:
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|||||||
76
core/cmd/dms/commands_auth.go
Normal file
76
core/cmd/dms/commands_auth.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authCmd = &cobra.Command{
|
||||||
|
Use: "auth",
|
||||||
|
Short: "Manage DMS authentication sync",
|
||||||
|
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
||||||
|
}
|
||||||
|
|
||||||
|
var authSyncCmd = &cobra.Command{
|
||||||
|
Use: "sync",
|
||||||
|
Short: "Sync DMS authentication configuration",
|
||||||
|
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
|
if term {
|
||||||
|
if err := syncAuthInTerminal(yes); err != nil {
|
||||||
|
log.Fatalf("Error launching auth sync in terminal: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := syncAuth(yes); err != nil {
|
||||||
|
log.Fatalf("Error syncing authentication: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
||||||
|
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncAuth(nonInteractive bool) error {
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Println("=== DMS Authentication Sync ===")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Println("\n=== Authentication Sync Complete ===")
|
||||||
|
fmt.Println("\nAuthentication changes have been applied.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncAuthInTerminal(nonInteractive bool) error {
|
||||||
|
syncFlags := make([]string, 0, 1)
|
||||||
|
if nonInteractive {
|
||||||
|
syncFlags = append(syncFlags, "--yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
shellSyncCmd := "dms auth sync"
|
||||||
|
if len(syncFlags) > 0 {
|
||||||
|
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
||||||
|
}
|
||||||
|
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
||||||
|
return runCommandInTerminal(shellCmd)
|
||||||
|
}
|
||||||
@@ -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()
|
defer ddc.Close()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||||
ddc.WaitPending()
|
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ Output format flags (mutually exclusive, default: --hex):
|
|||||||
--cmyk - CMYK values (C% M% Y% K%)
|
--cmyk - CMYK values (C% M% Y% K%)
|
||||||
--json - JSON with all formats
|
--json - JSON with all formats
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
dms color pick # Pick color, output as hex
|
dms color pick # Pick color, output as hex
|
||||||
dms color pick --rgb # Output as RGB
|
dms color pick --rgb # Output as RGB
|
||||||
@@ -53,6 +56,7 @@ func init() {
|
|||||||
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||||
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||||
|
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
|
||||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
@@ -113,7 +117,15 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
fmt.Println(output)
|
fmt.Println(output)
|
||||||
} else if color.IsDark() {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, _ := cmd.Flags().GetBool("raw"); raw {
|
||||||
|
fmt.Printf("%s\n", output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if color.IsDark() {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
|
|||||||
@@ -525,6 +525,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
randrCmd,
|
||||||
blurCmd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
@@ -83,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
@@ -91,7 +90,6 @@ var (
|
|||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -470,7 +468,6 @@ func checkWindowManagers() []checkResult {
|
|||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -503,7 +500,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -512,24 +509,9 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, checkCompositorBlurSupport())
|
|
||||||
|
|
||||||
return results
|
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 {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil && len(output) == 0 {
|
if err != nil && len(output) == 0 {
|
||||||
@@ -553,8 +535,6 @@ func detectRunningWM() string {
|
|||||||
return "Hyprland"
|
return "Hyprland"
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
return "niri"
|
return "niri"
|
||||||
case os.Getenv("MIRACLESOCK") != "":
|
|
||||||
return "Miracle WM"
|
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||||
}
|
}
|
||||||
@@ -573,7 +553,6 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
|||||||
qmlContent := `
|
qmlContent := `
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -582,7 +561,6 @@ ShellRoot {
|
|||||||
property bool idleMonitorAvailable: false
|
property bool idleMonitorAvailable: false
|
||||||
property bool idleInhibitorAvailable: false
|
property bool idleInhibitorAvailable: false
|
||||||
property bool shortcutInhibitorAvailable: false
|
property bool shortcutInhibitorAvailable: false
|
||||||
property bool backgroundBlurAvailable: false
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 50
|
interval: 50
|
||||||
@@ -600,18 +578,16 @@ ShellRoot {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var testItem = Qt.createQmlObject(
|
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 hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined"; ' +
|
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
||||||
'readonly property bool hasBackgroundBlur: typeof BackgroundEffect !== "undefined" ' +
|
|
||||||
'}',
|
'}',
|
||||||
root
|
root
|
||||||
)
|
)
|
||||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||||
root.backgroundBlurAvailable = testItem.hasBackgroundBlur
|
|
||||||
testItem.destroy()
|
testItem.destroy()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
@@ -620,8 +596,6 @@ ShellRoot {
|
|||||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor: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)])
|
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,7 +616,6 @@ ShellRoot {
|
|||||||
{"IdleMonitor", "Idle detection"},
|
{"IdleMonitor", "Idle detection"},
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -847,14 +820,10 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
results = append(results, checkImageFormatPlugins()...)
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
return !utils.CommandExists(t)
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||||
})
|
|
||||||
|
|
||||||
if len(terminals) > 0 {
|
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
|
||||||
} else {
|
} 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()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
@@ -1110,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
if currentCategory != -1 {
|
if currentCategory != -1 {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
||||||
currentCategory = r.category
|
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 != "" {
|
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("\n---\n")
|
||||||
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
|
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
|
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,41 +109,16 @@ func updateArchLinux() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packageName string
|
var packageName string
|
||||||
var isAUR bool
|
if isArchPackageInstalled("dms-shell-bin") {
|
||||||
if isArchPackageInstalled("dms-shell") {
|
packageName = "dms-shell-bin"
|
||||||
packageName = "dms-shell"
|
|
||||||
} else if isArchPackageInstalled("dms-shell-git") {
|
} else if isArchPackageInstalled("dms-shell-git") {
|
||||||
packageName = "dms-shell-git"
|
packageName = "dms-shell-git"
|
||||||
isAUR = true
|
|
||||||
} else if isArchPackageInstalled("dms-shell-bin") {
|
|
||||||
packageName = "dms-shell-bin"
|
|
||||||
isAUR = true
|
|
||||||
} else {
|
} 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...")
|
fmt.Println("Info: Falling back to git-based update method...")
|
||||||
return updateOtherDistros()
|
return updateOtherDistros()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isAUR {
|
|
||||||
fmt.Printf("This will update %s using pacman.\n", packageName)
|
|
||||||
if !confirmUpdate() {
|
|
||||||
return errdefs.ErrUpdateCancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
|
|
||||||
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
|
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); 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 helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
@@ -25,6 +26,11 @@ var greeterCmd = &cobra.Command{
|
|||||||
Long: "Manage DMS greeter (greetd)",
|
Long: "Manage DMS greeter (greetd)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
||||||
|
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
||||||
|
)
|
||||||
|
|
||||||
var greeterInstallCmd = &cobra.Command{
|
var greeterInstallCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
@@ -148,6 +154,16 @@ func init() {
|
|||||||
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
|
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
|
||||||
|
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if beforeAuth != nil {
|
||||||
|
beforeAuth()
|
||||||
|
}
|
||||||
|
return sharedAuthSyncFn(logFunc, "", options)
|
||||||
|
}
|
||||||
|
|
||||||
func installGreeter(nonInteractive bool) error {
|
func installGreeter(nonInteractive bool) error {
|
||||||
fmt.Println("=== DMS Greeter Installation ===")
|
fmt.Println("=== DMS Greeter Installation ===")
|
||||||
|
|
||||||
@@ -243,7 +259,9 @@ func installGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil {
|
if err := syncGreeterConfigsAndAuth(dmsPath, selectedCompositor, logFunc, sharedpam.SyncAuthOptions{}, func() {
|
||||||
|
fmt.Println("\nConfiguring authentication...")
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +296,7 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !nonInteractive {
|
if !nonInteractive {
|
||||||
fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS PAM managed block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ")
|
fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS-managed greeter auth block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ")
|
||||||
var response string
|
var response string
|
||||||
fmt.Scanln(&response)
|
fmt.Scanln(&response)
|
||||||
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
||||||
@@ -297,8 +315,8 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
fmt.Println(" ✓ greetd disabled")
|
fmt.Println(" ✓ greetd disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nRemoving DMS PAM configuration...")
|
fmt.Println("\nRemoving DMS authentication configuration...")
|
||||||
if err := greeter.RemoveGreeterPamManagedBlock(logFunc, ""); err != nil {
|
if err := sharedpam.RemoveManagedGreeterPamBlock(logFunc, ""); err != nil {
|
||||||
fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err)
|
fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +553,7 @@ func resolveLocalWrapperShell() (string, error) {
|
|||||||
|
|
||||||
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||||
if !nonInteractive {
|
if !nonInteractive {
|
||||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
fmt.Println("=== DMS Greeter Sync ===")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +739,11 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
if err := syncGreeterConfigsAndAuth(dmsPath, compositor, logFunc, sharedpam.SyncAuthOptions{
|
||||||
|
ForceGreeterAuth: forceAuth,
|
||||||
|
}, func() {
|
||||||
|
fmt.Println("\nConfiguring authentication...")
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,8 +756,9 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
|
|
||||||
fmt.Println("\n=== Sync Complete ===")
|
fmt.Println("\n=== Sync Complete ===")
|
||||||
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
||||||
|
fmt.Println("Shared authentication settings were also checked and reconciled where needed.")
|
||||||
if forceAuth {
|
if forceAuth {
|
||||||
fmt.Println("PAM has been configured for fingerprint and U2F (where modules exist).")
|
fmt.Println("Authentication has been configured for fingerprint and U2F (where modules exist).")
|
||||||
}
|
}
|
||||||
fmt.Println("The changes will be visible on the next login screen.")
|
fmt.Println("The changes will be visible on the next login screen.")
|
||||||
|
|
||||||
@@ -1297,39 +1320,7 @@ func extractGreeterPathOverrideFromCommand(command string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
||||||
if pamText == "" {
|
return sharedpam.ParseManagedGreeterPamAuth(pamText)
|
||||||
return false, false, false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(pamText, "\n")
|
|
||||||
inManaged := false
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
switch trimmed {
|
|
||||||
case greeter.GreeterPamManagedBlockStart:
|
|
||||||
managed = true
|
|
||||||
inManaged = true
|
|
||||||
continue
|
|
||||||
case greeter.GreeterPamManagedBlockEnd:
|
|
||||||
inManaged = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(trimmed, "# DMS greeter fingerprint") || strings.HasPrefix(trimmed, "# DMS greeter U2F") {
|
|
||||||
legacy = true
|
|
||||||
}
|
|
||||||
if !inManaged {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, "pam_fprintd") {
|
|
||||||
fingerprint = true
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, "pam_u2f") {
|
|
||||||
u2f = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return managed, fingerprint, u2f, legacy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func packageInstallHint() string {
|
func packageInstallHint() string {
|
||||||
@@ -1639,29 +1630,29 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
|
fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
|
||||||
}
|
}
|
||||||
if legacyManaged {
|
if legacyManaged {
|
||||||
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms auth sync' to normalize.")
|
||||||
allGood = false
|
allGood = false
|
||||||
}
|
}
|
||||||
enableFprintToggle, enableU2fToggle := false, false
|
enableFprintToggle, enableU2fToggle := false, false
|
||||||
if enableFprint, enableU2f, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil {
|
if enableFprint, enableU2f, settingsErr := sharedpam.ReadGreeterAuthToggles(homeDir); settingsErr == nil {
|
||||||
enableFprintToggle = enableFprint
|
enableFprintToggle = enableFprint
|
||||||
enableU2fToggle = enableU2f
|
enableU2fToggle = enableU2f
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr)
|
fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
includedFprintFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||||
includedU2fFile := greeter.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
|
includedU2fFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
|
||||||
fprintAvailableForCurrentUser := greeter.FingerprintAuthAvailableForCurrentUser()
|
fprintAvailableForCurrentUser := sharedpam.FingerprintAuthAvailableForCurrentUser()
|
||||||
|
|
||||||
if managedFprint && includedFprintFile != "" {
|
if managedFprint && includedFprintFile != "" {
|
||||||
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
||||||
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
|
fmt.Println(" Double fingerprint auth detected — run 'dms auth sync' to resolve.")
|
||||||
allGood = false
|
allGood = false
|
||||||
}
|
}
|
||||||
if managedU2f && includedU2fFile != "" {
|
if managedU2f && includedU2fFile != "" {
|
||||||
fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile)
|
fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile)
|
||||||
fmt.Println(" Double security-key auth detected — run 'dms greeter sync' to resolve.")
|
fmt.Println(" Double security-key auth detected — run 'dms auth sync' to resolve.")
|
||||||
allGood = false
|
allGood = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
core/cmd/dms/commands_greeter_test.go
Normal file
87
core/cmd/dms/commands_greeter_test.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
||||||
|
origGreeterConfigSyncFn := greeterConfigSyncFn
|
||||||
|
origSharedAuthSyncFn := sharedAuthSyncFn
|
||||||
|
t.Cleanup(func() {
|
||||||
|
greeterConfigSyncFn = origGreeterConfigSyncFn
|
||||||
|
sharedAuthSyncFn = origSharedAuthSyncFn
|
||||||
|
})
|
||||||
|
|
||||||
|
var calls []string
|
||||||
|
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
|
if dmsPath != "/tmp/dms" {
|
||||||
|
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
||||||
|
}
|
||||||
|
if compositor != "niri" {
|
||||||
|
t.Fatalf("unexpected compositor %q", compositor)
|
||||||
|
}
|
||||||
|
if sudoPassword != "" {
|
||||||
|
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
||||||
|
}
|
||||||
|
calls = append(calls, "configs")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotOptions sharedpam.SyncAuthOptions
|
||||||
|
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
||||||
|
if sudoPassword != "" {
|
||||||
|
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
||||||
|
}
|
||||||
|
gotOptions = options
|
||||||
|
calls = append(calls, "auth")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
||||||
|
ForceGreeterAuth: true,
|
||||||
|
}, func() {
|
||||||
|
calls = append(calls, "before-auth")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantCalls := []string{"configs", "before-auth", "auth"}
|
||||||
|
if !reflect.DeepEqual(calls, wantCalls) {
|
||||||
|
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
||||||
|
}
|
||||||
|
if !gotOptions.ForceGreeterAuth {
|
||||||
|
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
||||||
|
origGreeterConfigSyncFn := greeterConfigSyncFn
|
||||||
|
origSharedAuthSyncFn := sharedAuthSyncFn
|
||||||
|
t.Cleanup(func() {
|
||||||
|
greeterConfigSyncFn = origGreeterConfigSyncFn
|
||||||
|
sharedAuthSyncFn = origSharedAuthSyncFn
|
||||||
|
})
|
||||||
|
|
||||||
|
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
||||||
|
return errors.New("config sync failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
authCalled := false
|
||||||
|
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
||||||
|
authCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
||||||
|
if err == nil || err.Error() != "config sync failed" {
|
||||||
|
t.Fatalf("expected config sync error, got %v", err)
|
||||||
|
}
|
||||||
|
if authCalled {
|
||||||
|
t.Fatal("expected auth sync not to run after config sync failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,10 +57,11 @@ func init() {
|
|||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
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().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().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().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 {
|
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")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -87,6 +89,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
Mode: matugen.ColorMode(mode),
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
|
Contrast: contrast,
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
@@ -128,6 +131,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
"skipTemplates": opts.SkipTemplates,
|
||||||
|
"contrast": opts.Contrast,
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ var (
|
|||||||
ssNoClipboard bool
|
ssNoClipboard bool
|
||||||
ssNoFile bool
|
ssNoFile bool
|
||||||
ssNoNotify bool
|
ssNoNotify bool
|
||||||
|
ssNoConfirm bool
|
||||||
|
ssReset bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,8 +52,10 @@ Examples:
|
|||||||
dms screenshot output -o DP-1 # Specific output
|
dms screenshot output -o DP-1 # Specific output
|
||||||
dms screenshot window # Focused window (Hyprland)
|
dms screenshot window # Focused window (Hyprland)
|
||||||
dms screenshot last # Last region (pre-selected)
|
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-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
|
dms screenshot --no-confirm # Region capture on mouse release
|
||||||
dms screenshot --cursor=on # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
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(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
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(&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.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
screenshotCmd.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
@@ -142,6 +148,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config.Clipboard = !ssNoClipboard
|
config.Clipboard = !ssNoClipboard
|
||||||
config.SaveFile = !ssNoFile
|
config.SaveFile = !ssNoFile
|
||||||
config.Notify = !ssNoNotify
|
config.Notify = !ssNoNotify
|
||||||
|
config.NoConfirm = ssNoConfirm
|
||||||
|
config.Reset = ssReset
|
||||||
config.Stdout = ssStdout
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
if ssOutputDir != "" {
|
if ssOutputDir != "" {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,20 +17,20 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
|
authCmd.AddCommand(authSyncCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(authCmd)
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
clipboard.MaybeServeAndExit()
|
if os.Geteuid() == 0 {
|
||||||
|
|
||||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,17 +17,17 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
|
authCmd.AddCommand(authSyncCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
rootCmd.AddCommand(authCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
clipboard.MaybeServeAndExit()
|
if os.Geteuid() == 0 {
|
||||||
|
|
||||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
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() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
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() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,22 +7,6 @@ import (
|
|||||||
"strings"
|
"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 {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
10
core/go.mod
10
core/go.mod
@@ -1,6 +1,8 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
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/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
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 v1.7.16
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
@@ -32,15 +36,19 @@ require (
|
|||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // 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/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // 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/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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // 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/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.50.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/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
@@ -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.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
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/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 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 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/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
|
github.com/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
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/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/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.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 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
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
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,142 +13,66 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
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 {
|
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 {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
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 {
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if !foreground {
|
||||||
buf, err := io.ReadAll(data)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read source: %w", err)
|
|
||||||
}
|
|
||||||
return serveClipboard(buf, mimeType, pasteOnce)
|
|
||||||
}
|
}
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
return copyServeReader(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
cmd := exec.Command(os.Args[0])
|
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.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
||||||
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()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
var buf [1]byte
|
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
|
||||||
cacheFile, err := createClipboardCacheFile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create cache file: %w", err)
|
|
||||||
}
|
|
||||||
cachePath := cacheFile.Name()
|
|
||||||
|
|
||||||
if _, err := cacheFile.Write(data); err != nil {
|
|
||||||
cacheFile.Close()
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return fmt.Errorf("write cache file: %w", err)
|
|
||||||
}
|
|
||||||
if err := cacheFile.Close(); err != nil {
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return fmt.Errorf("close cache file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
if err := waitReady(cmd); err != nil {
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|
||||||
cmd := newForkCmd(mimeType, pasteOnce)
|
|
||||||
|
|
||||||
switch src := data.(type) {
|
switch src := data.(type) {
|
||||||
case *os.File:
|
case *os.File:
|
||||||
cmd.Stdin = src
|
cmd.Stdin = src
|
||||||
return waitReady(cmd)
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
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 {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
@@ -158,22 +83,50 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
if err := stdin.Close(); err != nil {
|
if err := stdin.Close(); err != nil {
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
if os.Getenv(envServe) == "" {
|
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.Stdout.Write([]byte{1})
|
os.Stdout.Write([]byte{1})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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) {
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
preferredDirs := []string{}
|
preferredDirs := []string{}
|
||||||
|
|
||||||
@@ -194,7 +147,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
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("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -236,10 +189,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -278,12 +233,18 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
sendErr := make(chan error, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
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")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
_, _ = file.Write(data)
|
if err := writeTo(file); err != nil {
|
||||||
|
select {
|
||||||
|
case sendErr <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -305,6 +266,8 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
|
case err := <-sendErr:
|
||||||
|
return err
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
@@ -558,10 +521,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -589,12 +554,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
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")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
_, _ = file.Write(data)
|
file.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
|||||||
|
|
||||||
# === Sizing & Layout ===
|
# === Sizing & Layout ===
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
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 ===
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
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 ^(pavucontrol)$
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
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 ^(gnome-calculator)$
|
||||||
windowrule = float on, match:class ^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"^org\.gnome\.Calculator$"#
|
|
||||||
match app-id=r#"^gnome-calculator$"#
|
match app-id=r#"^gnome-calculator$"#
|
||||||
match app-id=r#"^galculator$"#
|
match app-id=r#"^galculator$"#
|
||||||
match app-id=r#"^blueman-manager$"#
|
match app-id=r#"^blueman-manager$"#
|
||||||
|
|||||||
@@ -242,7 +242,11 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
|||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
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 {
|
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -324,13 +328,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
|
|
||||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
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
|
// Phase 3: System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -448,37 +445,6 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
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 := ExecSudoCommand(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 {
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -487,9 +453,6 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
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...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -577,7 +540,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
var dmsShell []string
|
var dmsShell []string
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "dms-shell-git" {
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
@@ -658,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")
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
depsToRemove := []string{
|
depsToRemove := []string{
|
||||||
"depends = quickshell",
|
"depends = quickshell",
|
||||||
@@ -681,7 +644,15 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
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{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
@@ -768,9 +739,42 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
CommandInfo: "sudo pacman -U built-package",
|
CommandInfo: "sudo pacman -U built-package",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||||
var files []string
|
var files []string
|
||||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
files = matches
|
// 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 {
|
if len(files) == 0 {
|
||||||
return fmt.Errorf("no package files found after building %s", pkg)
|
return fmt.Errorf("no package files found after building %s", pkg)
|
||||||
|
|||||||
@@ -103,15 +103,6 @@ func debianPackageInstalledPrecisely(pkg string) bool {
|
|||||||
return strings.TrimSpace(string(output)) == "installed"
|
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 {
|
func debianRepoArchitecture(arch string) string {
|
||||||
switch arch {
|
switch arch {
|
||||||
case "amd64", "x86_64":
|
case "amd64", "x86_64":
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -425,7 +426,7 @@ func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string
|
|||||||
|
|
||||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||||
for _, pkg := range extraPkgs {
|
for _, pkg := range extraPkgs {
|
||||||
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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,6 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
@@ -25,26 +26,7 @@ var appArmorProfileData []byte
|
|||||||
|
|
||||||
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
||||||
|
|
||||||
const (
|
const GreeterCacheDir = "/var/cache/dms-greeter"
|
||||||
GreeterCacheDir = "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
|
|
||||||
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
|
|
||||||
|
|
||||||
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
|
|
||||||
legacyGreeterPamU2FComment = "# DMS greeter U2F"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Common PAM auth stack names referenced by greetd across supported distros.
|
|
||||||
var includedPamAuthFiles = []string{
|
|
||||||
"system-auth",
|
|
||||||
"common-auth",
|
|
||||||
"password-auth",
|
|
||||||
"system-login",
|
|
||||||
"system-local-login",
|
|
||||||
"common-auth-pc",
|
|
||||||
"login",
|
|
||||||
}
|
|
||||||
|
|
||||||
func DetectDMSPath() (string, error) {
|
func DetectDMSPath() (string, error) {
|
||||||
return config.LocateDMSConfig()
|
return config.LocateDMSConfig()
|
||||||
@@ -630,7 +612,6 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
runtimeDirs := []string{
|
runtimeDirs := []string{
|
||||||
filepath.Join(cacheDir, ".local"),
|
filepath.Join(cacheDir, ".local"),
|
||||||
filepath.Join(cacheDir, ".local", "state"),
|
filepath.Join(cacheDir, ".local", "state"),
|
||||||
filepath.Join(cacheDir, ".local", "state", "wireplumber"),
|
|
||||||
filepath.Join(cacheDir, ".local", "share"),
|
filepath.Join(cacheDir, ".local", "share"),
|
||||||
filepath.Join(cacheDir, ".cache"),
|
filepath.Join(cacheDir, ".cache"),
|
||||||
}
|
}
|
||||||
@@ -750,49 +731,6 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveGreeterPamManagedBlock strips the DMS managed auth block from /etc/pam.d/greetd
|
|
||||||
func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) error {
|
|
||||||
if IsNixOS() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
const greetdPamPath = "/etc/pam.d/greetd"
|
|
||||||
data, err := os.ReadFile(greetdPamPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stripped, removed := stripManagedGreeterPamBlock(string(data))
|
|
||||||
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
|
||||||
if !removed && !removedLegacy {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp, err := os.CreateTemp("", "dms-pam-greetd-*")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create temp PAM file: %w", err)
|
|
||||||
}
|
|
||||||
tmpPath := tmp.Name()
|
|
||||||
defer os.Remove(tmpPath)
|
|
||||||
|
|
||||||
if _, err := tmp.WriteString(strippedAgain); err != nil {
|
|
||||||
tmp.Close()
|
|
||||||
return fmt.Errorf("failed to write temp PAM file: %w", err)
|
|
||||||
}
|
|
||||||
tmp.Close()
|
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to write PAM config: %w", err)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor.
|
// UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor.
|
||||||
// It is a no-op when AppArmor is not active or the profile does not exist.
|
// It is a no-op when AppArmor is not active or the profile does not exist.
|
||||||
func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||||
@@ -1323,7 +1261,7 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -1388,10 +1326,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syncGreeterPamConfig(homeDir, logFunc, sudoPassword, forceAuth); err != nil {
|
|
||||||
return fmt.Errorf("greeter PAM config sync failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToLower(compositor) != "niri" {
|
if strings.ToLower(compositor) != "niri" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1440,378 +1374,6 @@ func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPas
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pamModuleExists(module string) bool {
|
|
||||||
for _, libDir := range []string{
|
|
||||||
"/usr/lib64/security",
|
|
||||||
"/usr/lib/security",
|
|
||||||
"/lib64/security",
|
|
||||||
"/lib/security",
|
|
||||||
"/lib/x86_64-linux-gnu/security",
|
|
||||||
"/usr/lib/x86_64-linux-gnu/security",
|
|
||||||
"/lib/aarch64-linux-gnu/security",
|
|
||||||
"/usr/lib/aarch64-linux-gnu/security",
|
|
||||||
"/run/current-system/sw/lib64/security",
|
|
||||||
"/run/current-system/sw/lib/security",
|
|
||||||
} {
|
|
||||||
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripManagedGreeterPamBlock(content string) (string, bool) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
filtered := make([]string, 0, len(lines))
|
|
||||||
inManagedBlock := false
|
|
||||||
removed := false
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == GreeterPamManagedBlockStart {
|
|
||||||
inManagedBlock = true
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trimmed == GreeterPamManagedBlockEnd {
|
|
||||||
inManagedBlock = false
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inManagedBlock {
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(filtered, "\n"), removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripLegacyGreeterPamLines(content string) (string, bool) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
filtered := make([]string, 0, len(lines))
|
|
||||||
removed := false
|
|
||||||
|
|
||||||
for i := 0; i < len(lines); i++ {
|
|
||||||
trimmed := strings.TrimSpace(lines[i])
|
|
||||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
|
||||||
removed = true
|
|
||||||
if i+1 < len(lines) {
|
|
||||||
nextLine := strings.TrimSpace(lines[i+1])
|
|
||||||
if strings.HasPrefix(nextLine, "auth") &&
|
|
||||||
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, lines[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(filtered, "\n"), removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
|
|
||||||
block := strings.Join(blockLines, "\n")
|
|
||||||
prefix := strings.Join(lines[:i], "\n")
|
|
||||||
suffix := strings.Join(lines[i:], "\n")
|
|
||||||
switch {
|
|
||||||
case prefix == "":
|
|
||||||
return block + "\n" + suffix, nil
|
|
||||||
case suffix == "":
|
|
||||||
return prefix + "\n" + block, nil
|
|
||||||
default:
|
|
||||||
return prefix + "\n" + block + "\n" + suffix, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PamTextIncludesFile(pamText, filename string) bool {
|
|
||||||
lines := strings.Split(pamText, "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, filename) &&
|
|
||||||
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func PamFileHasModule(pamFilePath, module string) bool {
|
|
||||||
data, err := os.ReadFile(pamFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, module) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func DetectIncludedPamModule(pamText, module string) string {
|
|
||||||
for _, includedFile := range includedPamAuthFiles {
|
|
||||||
if PamTextIncludesFile(pamText, includedFile) && PamFileHasModule("/etc/pam.d/"+includedFile, module) {
|
|
||||||
return includedFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type greeterAuthSettings struct {
|
|
||||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
|
||||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func readGreeterAuthSettings(homeDir string) (greeterAuthSettings, error) {
|
|
||||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
|
||||||
data, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return greeterAuthSettings{}, nil
|
|
||||||
}
|
|
||||||
return greeterAuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(string(data)) == "" {
|
|
||||||
return greeterAuthSettings{}, nil
|
|
||||||
}
|
|
||||||
var settings greeterAuthSettings
|
|
||||||
if err := json.Unmarshal(data, &settings); err != nil {
|
|
||||||
return greeterAuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
return settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
|
||||||
settings, err := readGreeterAuthSettings(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasEnrolledFingerprintOutput(output string) bool {
|
|
||||||
lower := strings.ToLower(output)
|
|
||||||
if strings.Contains(lower, "no fingers enrolled") ||
|
|
||||||
strings.Contains(lower, "no fingerprints enrolled") ||
|
|
||||||
strings.Contains(lower, "no prints enrolled") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if strings.Contains(lower, "has fingers enrolled") ||
|
|
||||||
strings.Contains(lower, "has fingerprints enrolled") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(lower, "\n") {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(trimmed, "finger:") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func FingerprintAuthAvailableForUser(username string) bool {
|
|
||||||
username = strings.TrimSpace(username)
|
|
||||||
if username == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !pamModuleExists("pam_fprintd.so") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasEnrolledFingerprintOutput(string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
func FingerprintAuthAvailableForCurrentUser() bool {
|
|
||||||
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
|
||||||
if username == "" {
|
|
||||||
username = strings.TrimSpace(os.Getenv("USER"))
|
|
||||||
}
|
|
||||||
if username == "" {
|
|
||||||
out, err := exec.Command("id", "-un").Output()
|
|
||||||
if err == nil {
|
|
||||||
username = strings.TrimSpace(string(out))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FingerprintAuthAvailableForUser(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pamManagerHintForCurrentDistro() string {
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
|
||||||
if err != nil {
|
|
||||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
|
||||||
if !exists {
|
|
||||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
|
|
||||||
switch config.Family {
|
|
||||||
case distros.FamilyFedora:
|
|
||||||
return "Disable it in authselect to force password-only greeter login."
|
|
||||||
case distros.FamilyDebian, distros.FamilyUbuntu:
|
|
||||||
return "Disable it in pam-auth-update to force password-only greeter login."
|
|
||||||
default:
|
|
||||||
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
|
||||||
var wantFprint, wantU2f bool
|
|
||||||
fprintToggleEnabled := forceAuth
|
|
||||||
u2fToggleEnabled := forceAuth
|
|
||||||
if forceAuth {
|
|
||||||
wantFprint = pamModuleExists("pam_fprintd.so")
|
|
||||||
wantU2f = pamModuleExists("pam_u2f.so")
|
|
||||||
} else {
|
|
||||||
settings, err := readGreeterAuthSettings(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fprintToggleEnabled = settings.GreeterEnableFprint
|
|
||||||
u2fToggleEnabled = settings.GreeterEnableU2f
|
|
||||||
fprintModule := pamModuleExists("pam_fprintd.so")
|
|
||||||
u2fModule := pamModuleExists("pam_u2f.so")
|
|
||||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
|
||||||
wantU2f = settings.GreeterEnableU2f && u2fModule
|
|
||||||
if settings.GreeterEnableFprint && !fprintModule {
|
|
||||||
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
|
|
||||||
}
|
|
||||||
if settings.GreeterEnableU2f && !u2fModule {
|
|
||||||
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if IsNixOS() {
|
|
||||||
logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
|
|
||||||
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
greetdPamPath := "/etc/pam.d/greetd"
|
|
||||||
pamData, err := os.ReadFile(greetdPamPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
|
|
||||||
}
|
|
||||||
originalContent := string(pamData)
|
|
||||||
content, _ := stripManagedGreeterPamBlock(originalContent)
|
|
||||||
content, _ = stripLegacyGreeterPamLines(content)
|
|
||||||
|
|
||||||
includedFprintFile := DetectIncludedPamModule(content, "pam_fprintd.so")
|
|
||||||
includedU2fFile := DetectIncludedPamModule(content, "pam_u2f.so")
|
|
||||||
fprintAvailableForCurrentUser := FingerprintAuthAvailableForCurrentUser()
|
|
||||||
if wantFprint && includedFprintFile != "" {
|
|
||||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
|
||||||
wantFprint = false
|
|
||||||
}
|
|
||||||
if wantU2f && includedU2fFile != "" {
|
|
||||||
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
|
|
||||||
wantU2f = false
|
|
||||||
}
|
|
||||||
if !wantFprint && includedFprintFile != "" {
|
|
||||||
if fprintToggleEnabled {
|
|
||||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
|
||||||
if fprintAvailableForCurrentUser {
|
|
||||||
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
|
|
||||||
} else {
|
|
||||||
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if fprintAvailableForCurrentUser {
|
|
||||||
logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
|
|
||||||
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
|
|
||||||
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
|
|
||||||
} else {
|
|
||||||
logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
|
|
||||||
logFunc(" Password auth remains the effective login path.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !wantU2f && includedU2fFile != "" {
|
|
||||||
if u2fToggleEnabled {
|
|
||||||
logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
|
|
||||||
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
|
|
||||||
} else {
|
|
||||||
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
|
|
||||||
logFunc(" " + pamManagerHintForCurrentDistro())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wantFprint || wantU2f {
|
|
||||||
blockLines := []string{GreeterPamManagedBlockStart}
|
|
||||||
if wantFprint {
|
|
||||||
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
|
|
||||||
}
|
|
||||||
if wantU2f {
|
|
||||||
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
|
|
||||||
}
|
|
||||||
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
|
|
||||||
|
|
||||||
content, err = insertManagedGreeterPamBlock(content, blockLines, greetdPamPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if content == originalContent {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "greetd-pam-*.conf")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpPath := tmpFile.Name()
|
|
||||||
defer os.Remove(tmpPath)
|
|
||||||
if _, err := tmpFile.WriteString(content); err != nil {
|
|
||||||
tmpFile.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tmpFile.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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 := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
|
||||||
}
|
|
||||||
if wantFprint || wantU2f {
|
|
||||||
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
|
|
||||||
} else {
|
|
||||||
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type niriGreeterSync struct {
|
type niriGreeterSync struct {
|
||||||
processed map[string]bool
|
processed map[string]bool
|
||||||
nodes []*document.Node
|
nodes []*document.Node
|
||||||
@@ -2219,8 +1781,6 @@ vt = 1
|
|||||||
|
|
||||||
wrapperCmd := resolveGreeterWrapperPath()
|
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)
|
compositorLower := strings.ToLower(compositor)
|
||||||
commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
|
commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
|
||||||
if dmsPath != "" {
|
if dmsPath != "" {
|
||||||
@@ -2487,10 +2047,15 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
logFunc("Synchronizing DMS configurations...")
|
logFunc("Synchronizing DMS configurations...")
|
||||||
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword, false); err != nil {
|
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logFunc("Configuring authentication...")
|
||||||
|
if err := sharedpam.SyncAuthConfig(logFunc, sudoPassword, sharedpam.SyncAuthOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to sync authentication: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
logFunc("Checking for conflicting display managers...")
|
logFunc("Checking for conflicting display managers...")
|
||||||
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
|
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeTestJSON(t *testing.T, path string, content string) {
|
func writeTestFile(t *testing.T, path string, content string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||||
@@ -70,8 +70,8 @@ func TestResolveGreeterThemeSyncState(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
homeDir := t.TempDir()
|
homeDir := t.TempDir()
|
||||||
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||||
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||||
|
|
||||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,7 +292,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
parser := NewNiriParser(filepath.Dir(overridePath))
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
parser.currentSource = overridePath
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
doc, err := parseKDL(data)
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,103 +50,6 @@ type NiriParser struct {
|
|||||||
conflictingConfigs map[string]*NiriKeyBinding
|
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 {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
@@ -188,7 +91,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := parseKDL(data)
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,74 +3,9 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"testing"
|
"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) {
|
func TestNiriParseKeyCombo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
combo string
|
combo string
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ type Options struct {
|
|||||||
Mode ColorMode
|
Mode ColorMode
|
||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
|
Contrast float64
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
ColorsOnly bool
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
@@ -228,6 +229,7 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
|
|
||||||
log.Info("Running matugen color hex with stock color overrides")
|
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 := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
|
args = appendContrastArg(args, opts.Contrast)
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -264,6 +266,7 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
args = []string{opts.Kind, opts.Value}
|
args = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
|
args = appendContrastArg(args, opts.Contrast)
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -299,6 +302,13 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return true, nil
|
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 {
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
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 = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
|
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||||
if flags.isV4 {
|
if flags.isV4 {
|
||||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
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
|
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
|
// GetWiredConnections provides a mock function with no fields
|
||||||
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
|
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
892
core/internal/pam/pam.go
Normal file
892
core/internal/pam/pam.go
Normal file
@@ -0,0 +1,892 @@
|
|||||||
|
package pam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
|
||||||
|
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
|
||||||
|
|
||||||
|
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
|
||||||
|
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
|
||||||
|
|
||||||
|
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
|
||||||
|
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
|
||||||
|
|
||||||
|
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
|
||||||
|
legacyGreeterPamU2FComment = "# DMS greeter U2F"
|
||||||
|
|
||||||
|
GreetdPamPath = "/etc/pam.d/greetd"
|
||||||
|
DankshellPamPath = "/etc/pam.d/dankshell"
|
||||||
|
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
|
||||||
|
)
|
||||||
|
|
||||||
|
var includedPamAuthFiles = []string{
|
||||||
|
"system-auth",
|
||||||
|
"common-auth",
|
||||||
|
"password-auth",
|
||||||
|
"system-login",
|
||||||
|
"system-local-login",
|
||||||
|
"common-auth-pc",
|
||||||
|
"login",
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthSettings struct {
|
||||||
|
EnableFprint bool `json:"enableFprint"`
|
||||||
|
EnableU2f bool `json:"enableU2f"`
|
||||||
|
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||||
|
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncAuthOptions struct {
|
||||||
|
HomeDir string
|
||||||
|
ForceGreeterAuth bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type syncDeps struct {
|
||||||
|
pamDir string
|
||||||
|
greetdPath string
|
||||||
|
dankshellPath string
|
||||||
|
dankshellU2fPath string
|
||||||
|
isNixOS func() bool
|
||||||
|
readFile func(string) ([]byte, error)
|
||||||
|
stat func(string) (os.FileInfo, error)
|
||||||
|
createTemp func(string, string) (*os.File, error)
|
||||||
|
removeFile func(string) error
|
||||||
|
runSudoCmd func(string, string, ...string) error
|
||||||
|
pamModuleExists func(string) bool
|
||||||
|
fingerprintAvailableForCurrentUser func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type lockscreenPamIncludeDirective struct {
|
||||||
|
target string
|
||||||
|
filterType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type lockscreenPamResolver struct {
|
||||||
|
pamDir string
|
||||||
|
readFile func(string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSyncDeps() syncDeps {
|
||||||
|
return syncDeps{
|
||||||
|
pamDir: "/etc/pam.d",
|
||||||
|
greetdPath: GreetdPamPath,
|
||||||
|
dankshellPath: DankshellPamPath,
|
||||||
|
dankshellU2fPath: DankshellU2FPamPath,
|
||||||
|
isNixOS: IsNixOS,
|
||||||
|
readFile: os.ReadFile,
|
||||||
|
stat: os.Stat,
|
||||||
|
createTemp: os.CreateTemp,
|
||||||
|
removeFile: os.Remove,
|
||||||
|
runSudoCmd: runSudoCmd,
|
||||||
|
pamModuleExists: pamModuleExists,
|
||||||
|
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNixOS() bool {
|
||||||
|
_, err := os.Stat("/etc/NIXOS")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAuthSettings(homeDir string) (AuthSettings, error) {
|
||||||
|
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||||
|
data, err := os.ReadFile(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return AuthSettings{}, nil
|
||||||
|
}
|
||||||
|
return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(data)) == "" {
|
||||||
|
return AuthSettings{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings AuthSettings
|
||||||
|
if err := json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return AuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
||||||
|
settings, err := ReadAuthSettings(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
|
||||||
|
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
|
||||||
|
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
|
||||||
|
homeDir := strings.TrimSpace(options.HomeDir)
|
||||||
|
if homeDir == "" {
|
||||||
|
var err error
|
||||||
|
homeDir, err = os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := ReadAuthSettings(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := deps.stat(deps.greetdPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
||||||
|
if deps.isNixOS() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := deps.readFile(deps.greetdPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalContent := string(data)
|
||||||
|
stripped, removed := stripManagedGreeterPamBlock(originalContent)
|
||||||
|
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
||||||
|
if !removed && !removedLegacy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
||||||
|
if pamText == "" {
|
||||||
|
return false, false, false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(pamText, "\n")
|
||||||
|
inManaged := false
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
switch trimmed {
|
||||||
|
case GreeterPamManagedBlockStart:
|
||||||
|
managed = true
|
||||||
|
inManaged = true
|
||||||
|
continue
|
||||||
|
case GreeterPamManagedBlockEnd:
|
||||||
|
inManaged = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
||||||
|
legacy = true
|
||||||
|
}
|
||||||
|
if !inManaged {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(trimmed, "pam_fprintd") {
|
||||||
|
fingerprint = true
|
||||||
|
}
|
||||||
|
if strings.Contains(trimmed, "pam_u2f") {
|
||||||
|
u2f = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return managed, fingerprint, u2f, legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
func StripManagedGreeterPamContent(pamText string) (string, bool) {
|
||||||
|
stripped, removed := stripManagedGreeterPamBlock(pamText)
|
||||||
|
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
||||||
|
return stripped, removed || removedLegacy
|
||||||
|
}
|
||||||
|
|
||||||
|
func PamTextIncludesFile(pamText, filename string) bool {
|
||||||
|
lines := strings.Split(pamText, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(trimmed, filename) &&
|
||||||
|
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func PamFileHasModule(pamFilePath, module string) bool {
|
||||||
|
data, err := os.ReadFile(pamFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pamContentHasModule(string(data), module)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectIncludedPamModule(pamText, module string) string {
|
||||||
|
return detectIncludedPamModule(pamText, module, defaultSyncDeps())
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectIncludedPamModule(pamText, module string, deps syncDeps) string {
|
||||||
|
for _, includedFile := range includedPamAuthFiles {
|
||||||
|
if !PamTextIncludesFile(pamText, includedFile) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(deps.pamDir, includedFile)
|
||||||
|
data, err := deps.readFile(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pamContentHasModule(string(data), module) {
|
||||||
|
return includedFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func pamContentHasModule(content, module string) bool {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(trimmed, module) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasManagedLockscreenPamFile(content string) bool {
|
||||||
|
return strings.Contains(content, LockscreenPamManagedBlockStart) &&
|
||||||
|
strings.Contains(content, LockscreenPamManagedBlockEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasManagedLockscreenU2FPamFile(content string) bool {
|
||||||
|
return strings.Contains(content, LockscreenU2FPamManagedBlockStart) &&
|
||||||
|
strings.Contains(content, LockscreenU2FPamManagedBlockEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pamDirectiveType(line string) string {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
directiveType := strings.TrimPrefix(fields[0], "-")
|
||||||
|
switch directiveType {
|
||||||
|
case "auth", "account", "password", "session":
|
||||||
|
return directiveType
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExcludedLockscreenPamLine(line string) bool {
|
||||||
|
for _, field := range strings.Fields(line) {
|
||||||
|
if strings.HasPrefix(field, "#") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) {
|
||||||
|
fields := strings.Fields(trimmed)
|
||||||
|
if len(fields) >= 2 && fields[0] == "@include" {
|
||||||
|
return lockscreenPamIncludeDirective{
|
||||||
|
target: fields[1],
|
||||||
|
filterType: inheritedFilter,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") {
|
||||||
|
lineType := pamDirectiveType(trimmed)
|
||||||
|
if lineType == "" {
|
||||||
|
return lockscreenPamIncludeDirective{}, false
|
||||||
|
}
|
||||||
|
return lockscreenPamIncludeDirective{
|
||||||
|
target: fields[2],
|
||||||
|
filterType: lineType,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) >= 3 && fields[1] == "@include" {
|
||||||
|
lineType := pamDirectiveType(trimmed)
|
||||||
|
if lineType == "" {
|
||||||
|
return lockscreenPamIncludeDirective{}, false
|
||||||
|
}
|
||||||
|
return lockscreenPamIncludeDirective{
|
||||||
|
target: fields[2],
|
||||||
|
filterType: lineType,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return lockscreenPamIncludeDirective{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) {
|
||||||
|
if strings.TrimSpace(target) == "" {
|
||||||
|
return "", fmt.Errorf("empty PAM include target")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPamDir := filepath.Clean(pamDir)
|
||||||
|
if filepath.IsAbs(target) {
|
||||||
|
cleanTarget := filepath.Clean(target)
|
||||||
|
if filepath.Dir(cleanTarget) != cleanPamDir {
|
||||||
|
return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target)
|
||||||
|
}
|
||||||
|
return cleanTarget, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanTarget := filepath.Clean(target)
|
||||||
|
if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) {
|
||||||
|
return "", fmt.Errorf("invalid PAM include target: %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(cleanPamDir, cleanTarget), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) {
|
||||||
|
path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, seen := range stack {
|
||||||
|
if seen == path {
|
||||||
|
chain := append(append([]string{}, stack...), path)
|
||||||
|
display := make([]string, 0, len(chain))
|
||||||
|
for _, item := range chain {
|
||||||
|
display = append(display, filepath.Base(item))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := r.readFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved []string
|
||||||
|
for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
|
||||||
|
rawLine = strings.TrimRight(rawLine, "\r")
|
||||||
|
trimmed := strings.TrimSpace(rawLine)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
|
||||||
|
lineType := pamDirectiveType(trimmed)
|
||||||
|
if filterType != "" && lineType != "" && lineType != filterType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nested, err := r.resolveService(include.target, include.filterType, append(stack, path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolved = append(resolved, nested...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lineType := pamDirectiveType(trimmed)
|
||||||
|
if lineType == "" {
|
||||||
|
return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed)
|
||||||
|
}
|
||||||
|
if filterType != "" && lineType != filterType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isExcludedLockscreenPamLine(trimmed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = append(resolved, rawLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) {
|
||||||
|
resolver := lockscreenPamResolver{
|
||||||
|
pamDir: pamDir,
|
||||||
|
readFile: readFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedLines, err := resolver.resolveService("login", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(resolvedLines) == 0 {
|
||||||
|
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAuth := false
|
||||||
|
for _, line := range resolvedLines {
|
||||||
|
if pamDirectiveType(strings.TrimSpace(line)) == "auth" {
|
||||||
|
hasAuth = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasAuth {
|
||||||
|
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("#%PAM-1.0\n")
|
||||||
|
b.WriteString(LockscreenPamManagedBlockStart + "\n")
|
||||||
|
for _, line := range resolvedLines {
|
||||||
|
b.WriteString(line)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
b.WriteString(LockscreenPamManagedBlockEnd + "\n")
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildManagedLockscreenU2FPamContent() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("#%PAM-1.0\n")
|
||||||
|
b.WriteString(LockscreenU2FPamManagedBlockStart + "\n")
|
||||||
|
b.WriteString("auth required pam_u2f.so cue nouserok timeout=10\n")
|
||||||
|
b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
||||||
|
if deps.isNixOS() {
|
||||||
|
logFunc("ℹ NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
existingData, err := deps.readFile(deps.dankshellPath)
|
||||||
|
if err == nil {
|
||||||
|
if !hasManagedLockscreenPamFile(string(existingData)) {
|
||||||
|
logFunc("ℹ Custom /etc/pam.d/dankshell found (no DMS block). Skipping.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error {
|
||||||
|
if deps.isNixOS() {
|
||||||
|
logFunc("ℹ NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
existingData, err := deps.readFile(deps.dankshellU2fPath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
|
||||||
|
logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err)
|
||||||
|
}
|
||||||
|
logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
|
||||||
|
logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err)
|
||||||
|
}
|
||||||
|
logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripManagedGreeterPamBlock(content string) (string, bool) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
filtered := make([]string, 0, len(lines))
|
||||||
|
inManagedBlock := false
|
||||||
|
removed := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == GreeterPamManagedBlockStart {
|
||||||
|
inManagedBlock = true
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if trimmed == GreeterPamManagedBlockEnd {
|
||||||
|
inManagedBlock = false
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inManagedBlock {
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(filtered, "\n"), removed
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripLegacyGreeterPamLines(content string) (string, bool) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
filtered := make([]string, 0, len(lines))
|
||||||
|
removed := false
|
||||||
|
|
||||||
|
for i := 0; i < len(lines); i++ {
|
||||||
|
trimmed := strings.TrimSpace(lines[i])
|
||||||
|
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
||||||
|
removed = true
|
||||||
|
if i+1 < len(lines) {
|
||||||
|
nextLine := strings.TrimSpace(lines[i+1])
|
||||||
|
if strings.HasPrefix(nextLine, "auth") &&
|
||||||
|
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, lines[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(filtered, "\n"), removed
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
|
||||||
|
block := strings.Join(blockLines, "\n")
|
||||||
|
prefix := strings.Join(lines[:i], "\n")
|
||||||
|
suffix := strings.Join(lines[i:], "\n")
|
||||||
|
switch {
|
||||||
|
case prefix == "":
|
||||||
|
return block + "\n" + suffix, nil
|
||||||
|
case suffix == "":
|
||||||
|
return prefix + "\n" + block, nil
|
||||||
|
default:
|
||||||
|
return prefix + "\n" + block + "\n" + suffix, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
|
||||||
|
var wantFprint, wantU2f bool
|
||||||
|
fprintToggleEnabled := forceAuth
|
||||||
|
u2fToggleEnabled := forceAuth
|
||||||
|
if forceAuth {
|
||||||
|
wantFprint = deps.pamModuleExists("pam_fprintd.so")
|
||||||
|
wantU2f = deps.pamModuleExists("pam_u2f.so")
|
||||||
|
} else {
|
||||||
|
fprintToggleEnabled = settings.GreeterEnableFprint
|
||||||
|
u2fToggleEnabled = settings.GreeterEnableU2f
|
||||||
|
fprintModule := deps.pamModuleExists("pam_fprintd.so")
|
||||||
|
u2fModule := deps.pamModuleExists("pam_u2f.so")
|
||||||
|
wantFprint = settings.GreeterEnableFprint && fprintModule
|
||||||
|
wantU2f = settings.GreeterEnableU2f && u2fModule
|
||||||
|
if settings.GreeterEnableFprint && !fprintModule {
|
||||||
|
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
|
||||||
|
}
|
||||||
|
if settings.GreeterEnableU2f && !u2fModule {
|
||||||
|
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if deps.isNixOS() {
|
||||||
|
logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
|
||||||
|
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pamData, err := deps.readFile(deps.greetdPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
||||||
|
}
|
||||||
|
originalContent := string(pamData)
|
||||||
|
content, _ := stripManagedGreeterPamBlock(originalContent)
|
||||||
|
content, _ = stripLegacyGreeterPamLines(content)
|
||||||
|
|
||||||
|
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
|
||||||
|
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
|
||||||
|
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
|
||||||
|
if wantFprint && includedFprintFile != "" {
|
||||||
|
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
||||||
|
wantFprint = false
|
||||||
|
}
|
||||||
|
if wantU2f && includedU2fFile != "" {
|
||||||
|
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
|
||||||
|
wantU2f = false
|
||||||
|
}
|
||||||
|
if !wantFprint && includedFprintFile != "" {
|
||||||
|
if fprintToggleEnabled {
|
||||||
|
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
||||||
|
if fprintAvailableForCurrentUser {
|
||||||
|
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
|
||||||
|
} else {
|
||||||
|
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if fprintAvailableForCurrentUser {
|
||||||
|
logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
|
||||||
|
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
|
||||||
|
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
|
||||||
|
} else {
|
||||||
|
logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
|
||||||
|
logFunc(" Password auth remains the effective login path.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wantU2f && includedU2fFile != "" {
|
||||||
|
if u2fToggleEnabled {
|
||||||
|
logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
|
||||||
|
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
|
||||||
|
} else {
|
||||||
|
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
|
||||||
|
logFunc(" " + pamManagerHintForCurrentDistro())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wantFprint || wantU2f {
|
||||||
|
blockLines := []string{GreeterPamManagedBlockStart}
|
||||||
|
if wantFprint {
|
||||||
|
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
|
||||||
|
}
|
||||||
|
if wantU2f {
|
||||||
|
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
|
||||||
|
}
|
||||||
|
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
|
||||||
|
|
||||||
|
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if content == originalContent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
|
||||||
|
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
|
||||||
|
}
|
||||||
|
if wantFprint || wantU2f {
|
||||||
|
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
|
||||||
|
} else {
|
||||||
|
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
|
||||||
|
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
defer func() {
|
||||||
|
_ = deps.removeFile(tmpPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := tmpFile.WriteString(content); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tmpFile.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to set permissions on %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pamManagerHintForCurrentDistro() string {
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||||
|
}
|
||||||
|
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||||
|
if !exists {
|
||||||
|
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config.Family {
|
||||||
|
case distros.FamilyFedora:
|
||||||
|
return "Disable it in authselect to force password-only greeter login."
|
||||||
|
case distros.FamilyDebian, distros.FamilyUbuntu:
|
||||||
|
return "Disable it in pam-auth-update to force password-only greeter login."
|
||||||
|
default:
|
||||||
|
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pamModuleExists(module string) bool {
|
||||||
|
for _, libDir := range []string{
|
||||||
|
"/usr/lib64/security",
|
||||||
|
"/usr/lib/security",
|
||||||
|
"/lib64/security",
|
||||||
|
"/lib/security",
|
||||||
|
"/lib/x86_64-linux-gnu/security",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/security",
|
||||||
|
"/lib/aarch64-linux-gnu/security",
|
||||||
|
"/usr/lib/aarch64-linux-gnu/security",
|
||||||
|
"/run/current-system/sw/lib64/security",
|
||||||
|
"/run/current-system/sw/lib/security",
|
||||||
|
} {
|
||||||
|
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEnrolledFingerprintOutput(output string) bool {
|
||||||
|
lower := strings.ToLower(output)
|
||||||
|
if strings.Contains(lower, "no fingers enrolled") ||
|
||||||
|
strings.Contains(lower, "no fingerprints enrolled") ||
|
||||||
|
strings.Contains(lower, "no prints enrolled") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.Contains(lower, "has fingers enrolled") ||
|
||||||
|
strings.Contains(lower, "has fingerprints enrolled") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(lower, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "finger:") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FingerprintAuthAvailableForCurrentUser() bool {
|
||||||
|
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
||||||
|
if username == "" {
|
||||||
|
username = strings.TrimSpace(os.Getenv("USER"))
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
out, err := exec.Command("id", "-un").Output()
|
||||||
|
if err == nil {
|
||||||
|
username = strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fingerprintAuthAvailableForUser(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fingerprintAuthAvailableForUser(username string) bool {
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
if username == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !pamModuleExists("pam_fprintd.so") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasEnrolledFingerprintOutput(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
671
core/internal/pam/pam_test.go
Normal file
671
core/internal/pam/pam_test.go
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
package pam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTestFile(t *testing.T, path string, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pamTestEnv struct {
|
||||||
|
pamDir string
|
||||||
|
greetdPath string
|
||||||
|
dankshellPath string
|
||||||
|
dankshellU2fPath string
|
||||||
|
tmpDir string
|
||||||
|
homeDir string
|
||||||
|
availableModules map[string]bool
|
||||||
|
fingerprintAvailable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPamTestEnv(t *testing.T) *pamTestEnv {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
pamDir := filepath.Join(root, "pam.d")
|
||||||
|
tmpDir := filepath.Join(root, "tmp")
|
||||||
|
homeDir := filepath.Join(root, "home")
|
||||||
|
|
||||||
|
for _, dir := range []string{pamDir, tmpDir, homeDir} {
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pamTestEnv{
|
||||||
|
pamDir: pamDir,
|
||||||
|
greetdPath: filepath.Join(pamDir, "greetd"),
|
||||||
|
dankshellPath: filepath.Join(pamDir, "dankshell"),
|
||||||
|
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
|
||||||
|
tmpDir: tmpDir,
|
||||||
|
homeDir: homeDir,
|
||||||
|
availableModules: map[string]bool{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
|
||||||
|
t.Helper()
|
||||||
|
writeTestFile(t, filepath.Join(e.pamDir, name), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
|
||||||
|
t.Helper()
|
||||||
|
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
|
||||||
|
return syncDeps{
|
||||||
|
pamDir: e.pamDir,
|
||||||
|
greetdPath: e.greetdPath,
|
||||||
|
dankshellPath: e.dankshellPath,
|
||||||
|
dankshellU2fPath: e.dankshellU2fPath,
|
||||||
|
isNixOS: func() bool { return isNixOS },
|
||||||
|
readFile: os.ReadFile,
|
||||||
|
stat: os.Stat,
|
||||||
|
createTemp: func(_ string, pattern string) (*os.File, error) {
|
||||||
|
return os.CreateTemp(e.tmpDir, pattern)
|
||||||
|
},
|
||||||
|
removeFile: os.Remove,
|
||||||
|
runSudoCmd: func(_ string, command string, args ...string) error {
|
||||||
|
switch command {
|
||||||
|
case "cp":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("unexpected cp args: %v", args)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(args[1], data, 0o644)
|
||||||
|
case "chmod":
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("unexpected chmod args: %v", args)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "rm":
|
||||||
|
if len(args) != 2 || args[0] != "-f" {
|
||||||
|
return fmt.Errorf("unexpected rm args: %v", args)
|
||||||
|
}
|
||||||
|
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pamModuleExists: func(module string) bool {
|
||||||
|
return e.availableModules[module]
|
||||||
|
},
|
||||||
|
fingerprintAvailableForCurrentUser: func() bool {
|
||||||
|
return e.fingerprintAvailable
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFileString(t *testing.T, path string) string {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read %s: %v", path, err)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasManagedLockscreenPamFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both markers present",
|
||||||
|
content: "#%PAM-1.0\n" +
|
||||||
|
LockscreenPamManagedBlockStart + "\n" +
|
||||||
|
"auth sufficient pam_unix.so\n" +
|
||||||
|
LockscreenPamManagedBlockEnd + "\n",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing end marker is not managed",
|
||||||
|
content: "#%PAM-1.0\n" +
|
||||||
|
LockscreenPamManagedBlockStart + "\n" +
|
||||||
|
"auth sufficient pam_unix.so\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom file is not managed",
|
||||||
|
content: "#%PAM-1.0\nauth sufficient pam_unix.so\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := hasManagedLockscreenPamFile(tt.content); got != tt.want {
|
||||||
|
t.Fatalf("hasManagedLockscreenPamFile() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildManagedLockscreenPamContent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
files map[string]string
|
||||||
|
wantContains []string
|
||||||
|
wantNotContains []string
|
||||||
|
wantCounts map[string]int
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "preserves custom modules and strips direct u2f and fprint directives",
|
||||||
|
files: map[string]string{
|
||||||
|
"login": "#%PAM-1.0\n" +
|
||||||
|
"auth include system-auth\n" +
|
||||||
|
"account include system-auth\n" +
|
||||||
|
"session include system-auth\n",
|
||||||
|
"system-auth": "auth requisite pam_nologin.so\n" +
|
||||||
|
"auth sufficient pam_unix.so try_first_pass nullok\n" +
|
||||||
|
"auth sufficient pam_u2f.so cue\n" +
|
||||||
|
"auth sufficient pam_fprintd.so max-tries=1\n" +
|
||||||
|
"auth required pam_radius_auth.so conf=/etc/raddb/server\n" +
|
||||||
|
"account required pam_access.so\n" +
|
||||||
|
"session optional pam_lastlog.so silent\n",
|
||||||
|
},
|
||||||
|
wantContains: []string{
|
||||||
|
"#%PAM-1.0",
|
||||||
|
LockscreenPamManagedBlockStart,
|
||||||
|
LockscreenPamManagedBlockEnd,
|
||||||
|
"auth requisite pam_nologin.so",
|
||||||
|
"auth sufficient pam_unix.so try_first_pass nullok",
|
||||||
|
"auth required pam_radius_auth.so conf=/etc/raddb/server",
|
||||||
|
"account required pam_access.so",
|
||||||
|
"session optional pam_lastlog.so silent",
|
||||||
|
},
|
||||||
|
wantNotContains: []string{
|
||||||
|
"pam_u2f",
|
||||||
|
"pam_fprintd",
|
||||||
|
},
|
||||||
|
wantCounts: map[string]int{
|
||||||
|
"auth required pam_radius_auth.so conf=/etc/raddb/server": 1,
|
||||||
|
"account required pam_access.so": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolves nested include substack and @include transitively",
|
||||||
|
files: map[string]string{
|
||||||
|
"login": "#%PAM-1.0\n" +
|
||||||
|
"auth include system-auth\n" +
|
||||||
|
"account include system-auth\n" +
|
||||||
|
"password include system-auth\n" +
|
||||||
|
"session include system-auth\n",
|
||||||
|
"system-auth": "auth substack custom-auth\n" +
|
||||||
|
"account include custom-auth\n" +
|
||||||
|
"password include custom-auth\n" +
|
||||||
|
"session @include common-session\n",
|
||||||
|
"custom-auth": "auth required pam_custom.so one=two\n" +
|
||||||
|
"account required pam_custom_account.so\n" +
|
||||||
|
"password required pam_custom_password.so\n",
|
||||||
|
"common-session": "session optional pam_fprintd.so max-tries=1\n" +
|
||||||
|
"session optional pam_lastlog.so silent\n",
|
||||||
|
},
|
||||||
|
wantContains: []string{
|
||||||
|
"auth required pam_custom.so one=two",
|
||||||
|
"account required pam_custom_account.so",
|
||||||
|
"password required pam_custom_password.so",
|
||||||
|
"session optional pam_lastlog.so silent",
|
||||||
|
},
|
||||||
|
wantNotContains: []string{
|
||||||
|
"pam_fprintd",
|
||||||
|
},
|
||||||
|
wantCounts: map[string]int{
|
||||||
|
"auth required pam_custom.so one=two": 1,
|
||||||
|
"account required pam_custom_account.so": 1,
|
||||||
|
"password required pam_custom_password.so": 1,
|
||||||
|
"session optional pam_lastlog.so silent": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing include fails",
|
||||||
|
files: map[string]string{
|
||||||
|
"login": "#%PAM-1.0\nauth include missing-auth\n",
|
||||||
|
},
|
||||||
|
wantErr: "failed to read PAM file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclic include fails",
|
||||||
|
files: map[string]string{
|
||||||
|
"login": "#%PAM-1.0\nauth include system-auth\n",
|
||||||
|
"system-auth": "auth include login\n",
|
||||||
|
},
|
||||||
|
wantErr: "cyclic PAM include detected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no auth directives remain after filtering fails",
|
||||||
|
files: map[string]string{
|
||||||
|
"login": "#%PAM-1.0\nauth include system-auth\n",
|
||||||
|
"system-auth": "auth sufficient pam_u2f.so cue\n",
|
||||||
|
},
|
||||||
|
wantErr: "no auth directives remained after filtering",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
for name, content := range tt.files {
|
||||||
|
env.writePamFile(t, name, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := buildManagedLockscreenPamContent(env.pamDir, os.ReadFile)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
if !strings.Contains(content, want) {
|
||||||
|
t.Errorf("missing expected string %q in output:\n%s", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, notWant := range tt.wantNotContains {
|
||||||
|
if strings.Contains(content, notWant) {
|
||||||
|
t.Errorf("unexpected string %q found in output:\n%s", notWant, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for want, wantCount := range tt.wantCounts {
|
||||||
|
if gotCount := strings.Count(content, want); gotCount != wantCount {
|
||||||
|
t.Errorf("count for %q = %d, want %d\noutput:\n%s", want, gotCount, wantCount, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncLockscreenPamConfigWithDeps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("custom dankshell file is skipped untouched", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
customContent := "#%PAM-1.0\nauth required pam_unix.so\n"
|
||||||
|
env.writePamFile(t, "dankshell", customContent)
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := readFileString(t, env.dankshellPath); got != customContent {
|
||||||
|
t.Fatalf("custom dankshell content changed\ngot:\n%s\nwant:\n%s", got, customContent)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell found") {
|
||||||
|
t.Fatalf("expected custom-file skip log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("managed dankshell file is rewritten from resolved login stack", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
|
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\nauth sufficient pam_u2f.so cue\naccount required pam_access.so\n")
|
||||||
|
env.writePamFile(t, "dankshell", "#%PAM-1.0\n"+LockscreenPamManagedBlockStart+"\nauth required pam_env.so\n"+LockscreenPamManagedBlockEnd+"\n")
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := readFileString(t, env.dankshellPath)
|
||||||
|
for _, want := range []string{
|
||||||
|
LockscreenPamManagedBlockStart,
|
||||||
|
"auth sufficient pam_unix.so try_first_pass nullok",
|
||||||
|
"account required pam_access.so",
|
||||||
|
LockscreenPamManagedBlockEnd,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(output, want) {
|
||||||
|
t.Errorf("missing expected string %q in rewritten dankshell:\n%s", want, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(output, "pam_u2f") {
|
||||||
|
t.Errorf("rewritten dankshell still contains pam_u2f:\n%s", output)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell") {
|
||||||
|
t.Fatalf("expected success log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mutable systems fail when login stack cannot be converted safely", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
err := syncLockscreenPamConfigWithDeps(func(string) {}, "", env.deps(false))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when login PAM file is missing, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "failed to build") {
|
||||||
|
t.Fatalf("error = %q, want substring %q", err.Error(), "failed to build")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NixOS remains informational and does not write dankshell", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
var logs []string
|
||||||
|
|
||||||
|
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", env.deps(true))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncLockscreenPamConfigWithDeps returned error on NixOS path: %v", err)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "/etc/pam.d/login") {
|
||||||
|
t.Fatalf("expected NixOS informational log mentioning /etc/pam.d/login, got %v", logs)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected no dankshell file to be written on NixOS path, stat err = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("enabled creates managed file", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
var logs []string
|
||||||
|
|
||||||
|
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", true, env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFileString(t, env.dankshellU2fPath)
|
||||||
|
if got != buildManagedLockscreenU2FPamContent() {
|
||||||
|
t.Fatalf("unexpected managed dankshell-u2f content:\n%s", got)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell-u2f") {
|
||||||
|
t.Fatalf("expected create log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("enabled rewrites existing managed file", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\n"+LockscreenU2FPamManagedBlockStart+"\nauth required pam_u2f.so old\n"+LockscreenU2FPamManagedBlockEnd+"\n")
|
||||||
|
|
||||||
|
if err := syncLockscreenU2FPamConfigWithDeps(func(string) {}, "", true, env.deps(false)); err != nil {
|
||||||
|
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
||||||
|
t.Fatalf("managed dankshell-u2f was not rewritten:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disabled removes DMS-managed file", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", false, env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected managed dankshell-u2f to be removed, stat err = %v", err)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Removed DMS-managed /etc/pam.d/dankshell-u2f") {
|
||||||
|
t.Fatalf("expected removal log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disabled preserves custom file", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
customContent := "#%PAM-1.0\nauth required pam_u2f.so cue\n"
|
||||||
|
env.writePamFile(t, "dankshell-u2f", customContent)
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", false, env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got := readFileString(t, env.dankshellU2fPath); got != customContent {
|
||||||
|
t.Fatalf("custom dankshell-u2f content changed\ngot:\n%s\nwant:\n%s", got, customContent)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell-u2f found") {
|
||||||
|
t.Fatalf("expected custom-file log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_fprintd.so"] = true
|
||||||
|
env.availableModules["pam_u2f.so"] = true
|
||||||
|
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
|
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
|
||||||
|
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
||||||
|
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFileString(t, env.greetdPath)
|
||||||
|
for _, want := range []string{
|
||||||
|
GreeterPamManagedBlockStart,
|
||||||
|
"auth sufficient pam_fprintd.so max-tries=1 timeout=5",
|
||||||
|
"auth sufficient pam_u2f.so cue nouserok timeout=10",
|
||||||
|
GreeterPamManagedBlockEnd,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
|
||||||
|
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_fprintd.so"] = true
|
||||||
|
env.fingerprintAvailable = true
|
||||||
|
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
||||||
|
env.writePamFile(t, "greetd", original)
|
||||||
|
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
|
||||||
|
|
||||||
|
settings := AuthSettings{GreeterEnableFprint: true}
|
||||||
|
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
||||||
|
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFileString(t, env.greetdPath)
|
||||||
|
if got != original {
|
||||||
|
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, GreeterPamManagedBlockStart) {
|
||||||
|
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
|
||||||
|
legacyGreeterPamFprintComment+"\n"+
|
||||||
|
"auth sufficient pam_fprintd.so max-tries=1\n"+
|
||||||
|
GreeterPamManagedBlockStart+"\n"+
|
||||||
|
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
|
||||||
|
GreeterPamManagedBlockEnd+"\n"+
|
||||||
|
"auth include system-auth\n")
|
||||||
|
|
||||||
|
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
|
||||||
|
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFileString(t, env.greetdPath)
|
||||||
|
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
|
||||||
|
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "auth include system-auth") {
|
||||||
|
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.writeSettings(t, `{"enableU2f":true}`)
|
||||||
|
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
|
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
err := syncAuthConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(env.dankshellPath); err != nil {
|
||||||
|
t.Fatalf("expected dankshell to be created: %v", err)
|
||||||
|
}
|
||||||
|
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
||||||
|
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
|
||||||
|
t.Fatalf("expected greetd skip log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_fprintd.so"] = true
|
||||||
|
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
|
||||||
|
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
|
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
||||||
|
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||||
|
|
||||||
|
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dankshell := readFileString(t, env.dankshellPath)
|
||||||
|
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
|
||||||
|
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
greetd := readFileString(t, env.greetdPath)
|
||||||
|
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") {
|
||||||
|
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
|
||||||
|
}
|
||||||
|
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
|
||||||
|
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_fprintd.so"] = true
|
||||||
|
env.availableModules["pam_u2f.so"] = true
|
||||||
|
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
|
||||||
|
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
||||||
|
env.writePamFile(t, "greetd", originalGreetd)
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
err := syncAuthConfigWithDeps(func(msg string) {
|
||||||
|
logs = append(logs, msg)
|
||||||
|
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
|
||||||
|
}
|
||||||
|
if got := readFileString(t, env.greetdPath); got != originalGreetd {
|
||||||
|
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
|
||||||
|
}
|
||||||
|
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
|
||||||
|
t.Fatalf("expected informational NixOS logs, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -444,21 +444,20 @@ func GetFocusedMonitor() string {
|
|||||||
|
|
||||||
type outputInfo struct {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
scale float64
|
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllOutputInfos() map[string]*outputInfo {
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
ctx := display.Context()
|
ctx := display.Context()
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -477,17 +476,16 @@ func getAllOutputInfos() map[string]*outputInfo {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
name string
|
name string
|
||||||
x, y int32
|
x, y int32
|
||||||
scale float64
|
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
@@ -503,9 +501,6 @@ func getAllOutputInfos() map[string]*outputInfo {
|
|||||||
state.x = pe.X
|
state.x = pe.X
|
||||||
state.y = pe.Y
|
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) {
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
state.transform = te.Transform
|
state.transform = te.Transform
|
||||||
})
|
})
|
||||||
@@ -516,32 +511,21 @@ func getAllOutputInfos() map[string]*outputInfo {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]*outputInfo, len(heads))
|
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == "" {
|
if state.name == outputName {
|
||||||
continue
|
return &outputInfo{
|
||||||
}
|
x: state.x,
|
||||||
result[state.name] = &outputInfo{
|
y: state.y,
|
||||||
x: state.x,
|
transform: state.transform,
|
||||||
y: state.y,
|
}, true
|
||||||
scale: state.scale,
|
|
||||||
transform: state.transform,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
return nil, false
|
||||||
infos := getAllOutputInfos()
|
|
||||||
if infos == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
info, ok := infos[outputName]
|
|
||||||
return info, ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
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 {
|
if err := r.connect(); err != nil {
|
||||||
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ func (r *RegionSelector) setupPointerHandlers() {
|
|||||||
for _, os := range r.surfaces {
|
for _, os := range r.surfaces {
|
||||||
r.redrawSurface(os)
|
r.redrawSurface(os)
|
||||||
}
|
}
|
||||||
|
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
|
||||||
|
r.finishSelection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
r.cancelled = true
|
r.cancelled = true
|
||||||
|
|||||||
@@ -138,9 +138,13 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
|
|||||||
if !r.showCapturedCursor {
|
if !r.showCapturedCursor {
|
||||||
cursorLabel = "show"
|
cursorLabel = "show"
|
||||||
}
|
}
|
||||||
|
captureKey := "Space/Enter"
|
||||||
|
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
|
||||||
|
captureKey = "Drag+Release"
|
||||||
|
}
|
||||||
|
|
||||||
items := []struct{ key, desc string }{
|
items := []struct{ key, desc string }{
|
||||||
{"Space/Enter", "capture"},
|
{captureKey, "capture"},
|
||||||
{"P", cursorLabel + " cursor"},
|
{"P", cursorLabel + " cursor"},
|
||||||
{"Esc", "cancel"},
|
{"Esc", "cancel"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -107,6 +106,12 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureRegion() (*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)
|
selector := NewRegionSelector(s)
|
||||||
result, cancelled, err := selector.Run()
|
result, cancelled, err := selector.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -299,20 +304,22 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
return nil, fmt.Errorf("no outputs available")
|
return nil, fmt.Errorf("no outputs available")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(outputs) == 1 {
|
if len(outputs) == 1 {
|
||||||
return s.captureWholeOutput(outputs[0])
|
return s.captureWholeOutput(outputs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
wlrInfos := getAllOutputInfos()
|
// Capture all outputs first to get actual buffer sizes
|
||||||
|
type capturedOutput struct {
|
||||||
type pendingOutput struct {
|
output *WaylandOutput
|
||||||
result *CaptureResult
|
result *CaptureResult
|
||||||
logX float64
|
physX int
|
||||||
logY float64
|
physY int
|
||||||
scale float64
|
|
||||||
}
|
}
|
||||||
var pending []pendingOutput
|
captured := make([]capturedOutput, 0, len(outputs))
|
||||||
maxScale := 1.0
|
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
first := true
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -321,74 +328,50 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logX, logY := float64(output.x), float64(output.y)
|
outX, outY := output.x, output.y
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
|
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
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 {
|
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||||
scale = hs
|
scale = s
|
||||||
}
|
}
|
||||||
default:
|
case CompositorDWL:
|
||||||
if wlrInfos != nil {
|
if info, ok := getOutputInfo(output.name); ok {
|
||||||
if info, ok := wlrInfos[output.name]; ok {
|
outX, outY = info.x, info.y
|
||||||
logX, logY = float64(info.x), float64(info.y)
|
|
||||||
if info.scale > 0 {
|
|
||||||
scale = info.scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if scale <= 0 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
physX := int(float64(outX) * scale)
|
||||||
if scale > maxScale {
|
physY := int(float64(outY) * scale)
|
||||||
maxScale = scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pending) == 0 {
|
captured = append(captured, capturedOutput{
|
||||||
return nil, fmt.Errorf("failed to capture any outputs")
|
output: output,
|
||||||
}
|
result: result,
|
||||||
if len(pending) == 1 {
|
physX: physX,
|
||||||
return pending[0].result, nil
|
physY: physY,
|
||||||
}
|
})
|
||||||
|
|
||||||
type layoutEntry struct {
|
right := physX + result.Buffer.Width
|
||||||
result *CaptureResult
|
bottom := physY + result.Buffer.Height
|
||||||
canvasX int
|
|
||||||
canvasY int
|
|
||||||
canvasW int
|
|
||||||
canvasH int
|
|
||||||
}
|
|
||||||
entries := make([]layoutEntry, len(pending))
|
|
||||||
var minX, minY, maxX, maxY int
|
|
||||||
|
|
||||||
for i, p := range pending {
|
if first {
|
||||||
cx := int(math.Round(p.logX * maxScale))
|
minX, minY = physX, physY
|
||||||
cy := int(math.Round(p.logY * maxScale))
|
maxX, maxY = right, bottom
|
||||||
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
first = false
|
||||||
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
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cx < minX {
|
|
||||||
minX = cx
|
if physX < minX {
|
||||||
|
minX = physX
|
||||||
}
|
}
|
||||||
if cy < minY {
|
if physY < minY {
|
||||||
minY = cy
|
minY = physY
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
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
|
totalW := maxX - minX
|
||||||
totalH := maxY - minY
|
totalH := maxY - minY
|
||||||
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
|
||||||
|
compositeStride := totalW * 4
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, e := range entries {
|
for _, c := range captured {
|
||||||
e.result.Buffer.Close()
|
c.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composite.Clear()
|
composite.Clear()
|
||||||
|
|
||||||
var format uint32
|
var format uint32
|
||||||
for _, e := range entries {
|
for _, c := range captured {
|
||||||
if format == 0 {
|
if format == 0 {
|
||||||
format = e.result.Format
|
format = c.result.Format
|
||||||
}
|
}
|
||||||
s.blitBufferScaled(composite, e.result.Buffer,
|
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
||||||
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
c.result.Buffer.Close()
|
||||||
e.result.YInverted)
|
|
||||||
e.result.Buffer.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -427,44 +419,32 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||||
if dstW <= 0 || dstH <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
srcData := src.Data()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for dy := 0; dy < dstH; dy++ {
|
for srcY := 0; srcY < src.Height; srcY++ {
|
||||||
canvasY := dstY + dy
|
actualSrcY := srcY
|
||||||
if canvasY < 0 || canvasY >= dst.Height {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
srcY := dy * src.Height / dstH
|
|
||||||
if yInverted {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := srcY * src.Stride
|
srcRowOff := actualSrcY * src.Stride
|
||||||
dstRowOff := canvasY * dst.Stride
|
dstRowOff := dy * dst.Stride
|
||||||
|
|
||||||
for dx := 0; dx < dstW; dx++ {
|
for srcX := 0; srcX < src.Width; srcX++ {
|
||||||
canvasX := dstX + dx
|
dx := dstX + srcX
|
||||||
if canvasX < 0 || canvasX >= dst.Width {
|
if dx < 0 || dx >= dst.Width {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
srcX := dx * src.Width / dstW
|
|
||||||
if srcX >= src.Width {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + canvasX*4
|
di := dstRowOff + dx*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ type Config struct {
|
|||||||
Mode Mode
|
Mode Mode
|
||||||
OutputName string
|
OutputName string
|
||||||
Cursor CursorMode
|
Cursor CursorMode
|
||||||
|
NoConfirm bool
|
||||||
|
Reset bool
|
||||||
Format Format
|
Format Format
|
||||||
Quality int
|
Quality int
|
||||||
OutputDir string
|
OutputDir string
|
||||||
@@ -66,6 +68,8 @@ func DefaultConfig() Config {
|
|||||||
return Config{
|
return Config{
|
||||||
Mode: ModeRegion,
|
Mode: ModeRegion,
|
||||||
Cursor: CursorOff,
|
Cursor: CursorOff,
|
||||||
|
NoConfirm: false,
|
||||||
|
Reset: false,
|
||||||
Format: FormatPNG,
|
Format: FormatPNG,
|
||||||
Quality: 90,
|
Quality: 90,
|
||||||
OutputDir: "",
|
OutputDir: "",
|
||||||
|
|||||||
@@ -215,34 +215,31 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, exists := b.debounceTimers[id]; exists {
|
if timer, exists := b.debounceTimers[id]; exists {
|
||||||
if existing.Stop() {
|
timer.Reset(200 * time.Millisecond)
|
||||||
b.debounceWg.Done()
|
} 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()
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -493,19 +490,5 @@ func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
|
|||||||
return percent
|
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() {
|
func (b *DDCBackend) Close() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ type DDCBackend struct {
|
|||||||
debounceMutex sync.Mutex
|
debounceMutex sync.Mutex
|
||||||
debounceTimers map[string]*time.Timer
|
debounceTimers map[string]*time.Timer
|
||||||
debouncePending map[string]ddcPendingSet
|
debouncePending map[string]ddcPendingSet
|
||||||
debounceWg sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ddcPendingSet struct {
|
type ddcPendingSet struct {
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/pilebones/go-udev/netlink"
|
"github.com/pilebones/go-udev/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
udevRecvBufSize = 8 * 1024 * 1024
|
||||||
|
udevMaxRetries = 5
|
||||||
|
udevBaseDelay = 2 * time.Second
|
||||||
|
udevMaxDelay = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type UdevMonitor struct {
|
type UdevMonitor struct {
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
rescanMutex sync.Mutex
|
rescanMutex sync.Mutex
|
||||||
@@ -29,13 +37,6 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *UdevMonitor) run(manager *Manager) {
|
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{
|
matcher := &netlink.RuleDefinitions{
|
||||||
Rules: []netlink.RuleDefinition{
|
Rules: []netlink.RuleDefinition{
|
||||||
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||||
@@ -48,6 +49,46 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
return
|
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)
|
events := make(chan netlink.UEvent)
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
conn.Monitor(events, errs, matcher)
|
conn.Monitor(events, errs, matcher)
|
||||||
@@ -57,10 +98,9 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-m.stop:
|
case <-m.stop:
|
||||||
return
|
return nil
|
||||||
case err := <-errs:
|
case err := <-errs:
|
||||||
log.Errorf("Udev monitor error: %v", err)
|
return err
|
||||||
return
|
|
||||||
case event := <-events:
|
case event := <-events:
|
||||||
m.handleEvent(manager, event)
|
m.handleEvent(manager, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,10 +212,9 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var offer any
|
var offer any
|
||||||
switch {
|
if e.Id != nil {
|
||||||
case e.Id != nil:
|
|
||||||
offer = e.Id
|
offer = e.Id
|
||||||
case e.OfferId != 0:
|
} else if e.OfferId != 0 {
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
offer = m.offerRegistry[e.OfferId]
|
offer = m.offerRegistry[e.OfferId]
|
||||||
m.offerMutex.RUnlock()
|
m.offerMutex.RUnlock()
|
||||||
@@ -225,6 +224,10 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
wasOwner := m.isOwner
|
wasOwner := m.isOwner
|
||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
|
|
||||||
|
if offer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if wasOwner {
|
if wasOwner {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -233,11 +236,9 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
m.currentOffer = offer
|
m.currentOffer = offer
|
||||||
|
|
||||||
if prevOffer != nil && prevOffer != offer {
|
if prevOffer != nil && prevOffer != offer {
|
||||||
m.releaseOffer(prevOffer)
|
m.offerMutex.Lock()
|
||||||
}
|
delete(m.offerMimeTypes, prevOffer)
|
||||||
|
m.offerMutex.Unlock()
|
||||||
if offer == nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
@@ -291,33 +292,6 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
log.Info("Data device setup complete")
|
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) {
|
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
@@ -421,7 +395,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|||||||
if extractHash(v) != hash {
|
if extractHash(v) != hash {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -439,7 +413,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
|||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
var count int
|
var count int
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -482,14 +456,6 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeEntry(data []byte) (Entry, 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)
|
buf := bytes.NewReader(data)
|
||||||
var e Entry
|
var e Entry
|
||||||
|
|
||||||
@@ -497,15 +463,8 @@ func decodeEntryFields(data []byte, withData bool) (Entry, error) {
|
|||||||
|
|
||||||
var dataLen uint32
|
var dataLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||||
switch {
|
e.Data = make([]byte, dataLen)
|
||||||
case withData:
|
buf.Read(e.Data)
|
||||||
e.Data = make([]byte, dataLen)
|
|
||||||
buf.Read(e.Data)
|
|
||||||
default:
|
|
||||||
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
|
|
||||||
return e, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mimeLen uint32
|
var mimeLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||||
@@ -709,9 +668,14 @@ func sizeStr(size int) string {
|
|||||||
func (m *Manager) updateState() {
|
func (m *Manager) updateState() {
|
||||||
history := m.GetHistory()
|
history := m.GetHistory()
|
||||||
|
|
||||||
|
for i := range history {
|
||||||
|
history[i].Data = nil
|
||||||
|
}
|
||||||
|
|
||||||
var current *Entry
|
var current *Entry
|
||||||
if len(history) > 0 {
|
if len(history) > 0 {
|
||||||
c := history[0]
|
c := history[0]
|
||||||
|
c.Data = nil
|
||||||
current = &c
|
current = &c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,7 +750,7 @@ func (m *Manager) GetHistory() []Entry {
|
|||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
|
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -971,7 +935,7 @@ func (m *Manager) ClearHistory() {
|
|||||||
var toDelete [][]byte
|
var toDelete [][]byte
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
toDelete = append(toDelete, k)
|
toDelete = append(toDelete, k)
|
||||||
}
|
}
|
||||||
@@ -994,7 +958,7 @@ func (m *Manager) ClearHistory() {
|
|||||||
if b != nil {
|
if b != nil {
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, _ := decodeEntryMeta(v)
|
entry, _ := decodeEntry(v)
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1102,7 +1066,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
m.releaseCurrentSource()
|
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
m.sourceMutex.Lock()
|
m.sourceMutex.Lock()
|
||||||
m.sourceMimeTypes = []string{mimeType}
|
m.sourceMimeTypes = []string{mimeType}
|
||||||
@@ -1182,11 +1145,9 @@ func (m *Manager) Close() {
|
|||||||
m.subscribers = make(map[string]chan State)
|
m.subscribers = make(map[string]chan State)
|
||||||
m.subMutex.Unlock()
|
m.subMutex.Unlock()
|
||||||
|
|
||||||
m.releaseCurrentSource()
|
if m.currentSource != nil {
|
||||||
|
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||||
if m.currentOffer != nil {
|
source.Destroy()
|
||||||
m.releaseOffer(m.currentOffer)
|
|
||||||
m.currentOffer = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.dataDevice != nil {
|
if m.dataDevice != nil {
|
||||||
@@ -1230,10 +1191,11 @@ func (m *Manager) clearOldEntries(days int) error {
|
|||||||
var toDelete [][]byte
|
var toDelete [][]byte
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Skip pinned entries
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1348,7 +1310,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1373,6 +1335,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.Data = nil
|
||||||
all = append(all, entry)
|
all = append(all, entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1547,7 +1510,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1565,6 +1528,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check pinned count
|
||||||
cfg := m.getConfig()
|
cfg := m.getConfig()
|
||||||
pinnedCount := 0
|
pinnedCount := 0
|
||||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
@@ -1574,7 +1538,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1665,11 +1629,12 @@ func (m *Manager) GetPinnedEntries() []Entry {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
|
entry.Data = nil
|
||||||
pinned = append(pinned, entry)
|
pinned = append(pinned, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1695,7 +1660,7 @@ func (m *Manager) GetPinnedCount() int {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntryMeta(v)
|
entry, err := decodeEntry(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -1814,7 +1779,6 @@ func (m *Manager) CopyFile(filePath string) error {
|
|||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
m.releaseCurrentSource()
|
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
m.ownerLock.Lock()
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package cups
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -275,13 +277,42 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
|
|||||||
return classes, nil
|
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 {
|
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
|
||||||
usedPkHelper := false
|
usedPkHelper := false
|
||||||
|
|
||||||
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
||||||
if isAuthError(err) && m.pkHelper != nil {
|
if isAuthError(err) && m.pkHelper != nil {
|
||||||
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != 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
|
usedPkHelper = true
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -308,6 +339,12 @@ func (m *Manager) DeletePrinter(printerName string) error {
|
|||||||
err := m.client.DeletePrinter(printerName)
|
err := m.client.DeletePrinter(printerName)
|
||||||
if isAuthError(err) && m.pkHelper != nil {
|
if isAuthError(err) && m.pkHelper != nil {
|
||||||
err = m.pkHelper.PrinterDelete(printerName)
|
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 {
|
if err == nil {
|
||||||
m.RefreshState()
|
m.RefreshState()
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
handleRestartJob(conn, req, manager)
|
handleRestartJob(conn, req, manager)
|
||||||
case "cups.holdJob":
|
case "cups.holdJob":
|
||||||
handleHoldJob(conn, req, manager)
|
handleHoldJob(conn, req, manager)
|
||||||
|
case "cups.testConnection":
|
||||||
|
handleTestConnection(conn, req, manager)
|
||||||
default:
|
default:
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
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"})
|
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 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 {
|
type PrinterClass struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
@@ -77,6 +87,7 @@ type Manager struct {
|
|||||||
notifierWg sync.WaitGroup
|
notifierWg sync.WaitGroup
|
||||||
lastNotifiedState *CUPSState
|
lastNotifiedState *CUPSState
|
||||||
baseURL string
|
baseURL string
|
||||||
|
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscriptionManagerInterface interface {
|
type SubscriptionManagerInterface interface {
|
||||||
|
|||||||
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),
|
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||||
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||||
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||||
|
Contrast: models.GetOr(req, "contrast", 0.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
wait := models.GetOr(req, "wait", true)
|
wait := models.GetOr(req, "wait", true)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type Backend interface {
|
|||||||
ScanWiFi() error
|
ScanWiFi() error
|
||||||
ScanWiFiDevice(device string) error
|
ScanWiFiDevice(device string) error
|
||||||
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
|
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
|
||||||
|
GetWiFiQRCodeContent(ssid string) (string, error)
|
||||||
GetWiFiDevices() []WiFiDevice
|
GetWiFiDevices() []WiFiDevice
|
||||||
|
|
||||||
ConnectWiFi(req ConnectionRequest) error
|
ConnectWiFi(req ConnectionRequest) error
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkI
|
|||||||
return b.wifi.GetWiFiNetworkDetails(ssid)
|
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 {
|
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||||
if err := b.wifi.ConnectWiFi(req); err != nil {
|
if err := b.wifi.ConnectWiFi(req); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
return nil, fmt.Errorf("wired connections not supported by iwd")
|
||||||
@@ -112,3 +115,19 @@ func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
|
|||||||
Networks: b.state.WiFiNetworks,
|
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")
|
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 {
|
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||||
return fmt.Errorf("WiFi connect not supported by networkd backend")
|
return fmt.Errorf("WiFi connect not supported by networkd backend")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,26 +158,18 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
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{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: isConnected,
|
Connected: ssid == currentSSID && bssid == currentBSSID,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: rate,
|
Rate: maxBitrate / 1000,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +196,65 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
|||||||
}, nil
|
}, 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 {
|
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||||
devInfo, err := b.getWifiDeviceForConnection(req.Device)
|
devInfo, err := b.getWifiDeviceForConnection(req.Device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -463,27 +514,19 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
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{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: isConnected,
|
Connected: ssid == currentSSID,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Hidden: hiddenSSIDs[ssid],
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: rate,
|
Rate: maxBitrate / 1000,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1019,27 +1062,19 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
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{
|
network := WiFiNetwork{
|
||||||
SSID: apSSID,
|
SSID: apSSID,
|
||||||
BSSID: apBSSID,
|
BSSID: apBSSID,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: isConnected,
|
Connected: connected && apSSID == ssid,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
Hidden: hiddenSSIDs[apSSID],
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: rate,
|
Rate: maxBitrate / 1000,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
Device: name,
|
Device: name,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"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)
|
handleSetPreference(conn, req, manager)
|
||||||
case "network.info":
|
case "network.info":
|
||||||
handleGetNetworkInfo(conn, req, manager)
|
handleGetNetworkInfo(conn, req, manager)
|
||||||
|
case "network.qrcode":
|
||||||
|
handleGetNetworkQRCode(conn, req, manager)
|
||||||
|
case "network.delete-qrcode":
|
||||||
|
handleDeleteQRCode(conn, req, manager)
|
||||||
case "network.ethernet.info":
|
case "network.ethernet.info":
|
||||||
handleGetWiredNetworkInfo(conn, req, manager)
|
handleGetWiredNetworkInfo(conn, req, manager)
|
||||||
case "network.subscribe":
|
case "network.subscribe":
|
||||||
@@ -320,6 +325,42 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
models.Respond(conn, req.ID, network)
|
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) {
|
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
uuid, err := params.String(req.Params, "uuid")
|
uuid, err := params.String(req.Params, "uuid")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewManager() (*Manager, error) {
|
func NewManager() (*Manager, error) {
|
||||||
@@ -438,6 +440,43 @@ func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, err
|
|||||||
return m.backend.GetWiFiNetworkDetails(ssid)
|
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 {
|
func (m *Manager) ToggleWiFi() error {
|
||||||
enabled, err := m.backend.GetWiFiEnabled()
|
enabled, err := m.backend.GetWiFiEnabled()
|
||||||
if err != nil {
|
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/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
"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/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
@@ -192,6 +193,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
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 {
|
switch req.Method {
|
||||||
case "ping":
|
case "ping":
|
||||||
models.Respond(conn, req.ID, "pong")
|
models.Respond(conn, req.ID, "pong")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
"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/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
"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/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
@@ -70,6 +72,8 @@ var clipboardManager *clipboard.Manager
|
|||||||
var dbusManager *serverDbus.Manager
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
var themeModeManager *thememode.Manager
|
var themeModeManager *thememode.Manager
|
||||||
|
var locationManager *location.Manager
|
||||||
|
var geoClientInstance geolocation.Client
|
||||||
|
|
||||||
const dbusClientID = "dms-dbus-client"
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
@@ -390,6 +394,19 @@ func InitializeThemeModeManager() error {
|
|||||||
return nil
|
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) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -537,6 +554,10 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "theme.auto")
|
caps = append(caps, "theme.auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if locationManager != nil {
|
||||||
|
caps = append(caps, "location")
|
||||||
|
}
|
||||||
|
|
||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
caps = append(caps, "dbus")
|
caps = append(caps, "dbus")
|
||||||
}
|
}
|
||||||
@@ -1307,6 +1328,12 @@ func cleanupManagers() {
|
|||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
|
if locationManager != nil {
|
||||||
|
locationManager.Close()
|
||||||
|
}
|
||||||
|
if geoClientInstance != nil {
|
||||||
|
geoClientInstance.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(printDocs bool) error {
|
func Start(printDocs bool) error {
|
||||||
@@ -1488,6 +1515,9 @@ func Start(printDocs bool) error {
|
|||||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
||||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
||||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
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("")
|
||||||
}
|
}
|
||||||
log.Info("Initializing managers...")
|
log.Info("Initializing managers...")
|
||||||
@@ -1567,6 +1597,37 @@ func Start(printDocs bool) error {
|
|||||||
log.Warnf("Wayland manager unavailable: %v", err)
|
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() {
|
go func() {
|
||||||
if err := InitializeBluezManager(); err != nil {
|
if err := InitializeBluezManager(); err != nil {
|
||||||
log.Warnf("Bluez manager unavailable: %v", err)
|
log.Warnf("Bluez manager unavailable: %v", err)
|
||||||
@@ -1595,19 +1656,6 @@ func Start(printDocs bool) error {
|
|||||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
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)
|
fatalErrChan := make(chan error, 1)
|
||||||
if wlrOutputManager != nil {
|
if wlrOutputManager != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
@@ -32,6 +33,8 @@ type Manager struct {
|
|||||||
cachedIPLat *float64
|
cachedIPLat *float64
|
||||||
cachedIPLon *float64
|
cachedIPLon *float64
|
||||||
|
|
||||||
|
geoClient geolocation.Client
|
||||||
|
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
updateTrigger chan struct{}
|
updateTrigger chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -311,6 +314,10 @@ func (m *Manager) getConfig() Config {
|
|||||||
return m.config
|
return m.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetGeoClient(client geolocation.Client) {
|
||||||
|
m.geoClient = client
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||||
if config.Latitude != nil && config.Longitude != nil {
|
if config.Latitude != nil && config.Longitude != nil {
|
||||||
return config.Latitude, config.Longitude
|
return config.Latitude, config.Longitude
|
||||||
@@ -318,6 +325,9 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
|||||||
if !config.UseIPLocation {
|
if !config.UseIPLocation {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
if m.geoClient == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
m.locationMutex.RLock()
|
m.locationMutex.RLock()
|
||||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
@@ -327,17 +337,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
|||||||
}
|
}
|
||||||
m.locationMutex.RUnlock()
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
lat, lon, err := wayland.FetchIPLocation()
|
location, err := m.geoClient.GetLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.locationMutex.Lock()
|
m.locationMutex.Lock()
|
||||||
m.cachedIPLat = lat
|
m.cachedIPLat = &location.Latitude
|
||||||
m.cachedIPLon = lon
|
m.cachedIPLon = &location.Longitude
|
||||||
m.locationMutex.Unlock()
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
return lat, lon
|
return m.cachedIPLat, m.cachedIPLon
|
||||||
}
|
}
|
||||||
|
|
||||||
func statesEqual(a, b *State) bool {
|
func statesEqual(a, b *State) bool {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"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/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
"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) {
|
func (m *Manager) getLocation() (*float64, *float64) {
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
config := m.config
|
config := m.config
|
||||||
@@ -428,26 +433,31 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
|||||||
if config.Latitude != nil && config.Longitude != nil {
|
if config.Latitude != nil && config.Longitude != nil {
|
||||||
return config.Latitude, config.Longitude
|
return config.Latitude, config.Longitude
|
||||||
}
|
}
|
||||||
if config.UseIPLocation {
|
if !config.UseIPLocation {
|
||||||
m.locationMutex.RLock()
|
return nil, nil
|
||||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
}
|
||||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
if m.geoClient == nil {
|
||||||
m.locationMutex.RUnlock()
|
return nil, nil
|
||||||
return lat, lon
|
}
|
||||||
}
|
|
||||||
m.locationMutex.RUnlock()
|
|
||||||
|
|
||||||
lat, lon, err := FetchIPLocation()
|
m.locationMutex.RLock()
|
||||||
if err != nil {
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
return nil, nil
|
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||||
}
|
m.locationMutex.RUnlock()
|
||||||
m.locationMutex.Lock()
|
|
||||||
m.cachedIPLat = lat
|
|
||||||
m.cachedIPLon = lon
|
|
||||||
m.locationMutex.Unlock()
|
|
||||||
return lat, lon
|
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 {
|
func (m *Manager) hasValidSchedule() bool {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"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"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -97,6 +98,8 @@ type Manager struct {
|
|||||||
dbusConn *dbus.Conn
|
dbusConn *dbus.Conn
|
||||||
dbusSignal chan *dbus.Signal
|
dbusSignal chan *dbus.Signal
|
||||||
|
|
||||||
|
geoClient geolocation.Client
|
||||||
|
|
||||||
lastAppliedTemp int
|
lastAppliedTemp int
|
||||||
lastAppliedGamma float64
|
lastAppliedGamma float64
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
|
|||||||
|
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render("Setting up configuration files...")
|
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")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Show progress information
|
// Show progress information
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
|
|||||||
|
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
|
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()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
|
|||||||
if !m.packageProgress.isComplete {
|
if !m.packageProgress.isComplete {
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render(m.packageProgress.step)
|
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")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Show progress bar
|
// Show progress bar
|
||||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
|||||||
if isGit {
|
if isGit {
|
||||||
return "dms-shell-git"
|
return "dms-shell-git"
|
||||||
}
|
}
|
||||||
return "dms-shell"
|
return "dms-shell-bin"
|
||||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||||
if isGit {
|
if isGit {
|
||||||
return "dms-git"
|
return "dms-git"
|
||||||
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
|
|||||||
|
|
||||||
for i := startIdx; i < len(allLogs); i++ {
|
for i := startIdx; i < len(allLogs); i++ {
|
||||||
if allLogs[i] != "" {
|
if allLogs[i] != "" {
|
||||||
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
|
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
|
|||||||
|
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render("Waiting for fingerprint...")
|
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()
|
return b.String()
|
||||||
|
|||||||
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
|
|||||||
contentStyle = contentStyle.Bold(true)
|
contentStyle = contentStyle.Bold(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
fmt.Fprintf(&b, " %s %s\n",
|
||||||
prefixStyle.Render(prefix),
|
prefixStyle.Render(prefix),
|
||||||
contentStyle.Render(content)))
|
contentStyle.Render(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
|
|||||||
} else if m.isLoading {
|
} else if m.isLoading {
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
loading := m.styles.Normal.Render("Detecting system...")
|
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
|
// Footer with better visual separation
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ override_dh_auto_build:
|
|||||||
# Verify core directory exists (native package format has source at root)
|
# Verify core directory exists (native package format has source at root)
|
||||||
test -d core || (echo "ERROR: core directory not found!" && exit 1)
|
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)
|
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
||||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
|
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
|
# Build dms-cli (single shell to preserve variables; arch: Debian amd64/arm64 -> Makefile amd64/arm64)
|
||||||
# Extract version info and build in single shell to preserve variables
|
|
||||||
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
|
|
||||||
VERSION="$(UPSTREAM_VERSION)"; \
|
VERSION="$(UPSTREAM_VERSION)"; \
|
||||||
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \
|
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 \
|
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<service name="download_url">
|
<service name="download_url">
|
||||||
<param name="protocol">https</param>
|
<param name="protocol">https</param>
|
||||||
<param name="host">github.com</param>
|
<param name="host">github.com</param>
|
||||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/dms-qml.tar.gz</param>
|
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.3/dms-qml.tar.gz</param>
|
||||||
<param name="filename">dms-qml.tar.gz</param>
|
<param name="filename">dms-qml.tar.gz</param>
|
||||||
</service>
|
</service>
|
||||||
</services>
|
</services>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
dms-greeter (1.4.2db8) unstable; urgency=medium
|
dms-greeter (1.4.3db1) unstable; urgency=medium
|
||||||
|
|
||||||
* Initial Debian OBS package
|
* Update to v1.4.3 stable release
|
||||||
* Port from Ubuntu/Fedora packaging
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000
|
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
%global version {{{ git_repo_version }}}
|
%global version {{{ git_repo_version }}}
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
|
%global go_toolchain_version 1.26.1
|
||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Epoch: 2
|
Epoch: 2
|
||||||
@@ -14,12 +15,12 @@ License: MIT
|
|||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
VCS: {{{ git_repo_vcs }}}
|
VCS: {{{ git_repo_vcs }}}
|
||||||
Source0: {{{ git_repo_pack }}}
|
Source0: {{{ git_repo_pack }}}
|
||||||
|
Source1: https://go.dev/dl/go%{go_toolchain_version}.linux-amd64.tar.gz
|
||||||
|
Source2: https://go.dev/dl/go%{go_toolchain_version}.linux-arm64.tar.gz
|
||||||
|
|
||||||
BuildRequires: git-core
|
BuildRequires: git-core
|
||||||
BuildRequires: gzip
|
BuildRequires: gzip
|
||||||
BuildRequires: golang >= 1.24
|
|
||||||
BuildRequires: make
|
BuildRequires: make
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
# Core requirements
|
# Core requirements
|
||||||
@@ -28,7 +29,7 @@ Requires: accountsservice
|
|||||||
Requires: dms-cli = %{epoch}:%{version}-%{release}
|
Requires: dms-cli = %{epoch}:%{version}-%{release}
|
||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
# Core utilities (Highly recommended for DMS functionality)
|
# Core utilities (Recommended for DMS functionality)
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
@@ -66,6 +67,28 @@ Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|||||||
VERSION="%{version}"
|
VERSION="%{version}"
|
||||||
COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown")
|
COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown")
|
||||||
|
|
||||||
|
# Use pinned bundled Go toolchain (deterministic across chroots)
|
||||||
|
case "%{_arch}" in
|
||||||
|
x86_64)
|
||||||
|
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-amd64.tar.gz"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-arm64.tar.gz"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture for bundled Go: %{_arch}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
rm -rf .go
|
||||||
|
tar -xzf "$GO_TARBALL"
|
||||||
|
mv go .go
|
||||||
|
export GOROOT="$PWD/.go"
|
||||||
|
export PATH="$GOROOT/bin:$PATH"
|
||||||
|
export GOTOOLCHAIN=local
|
||||||
|
go version
|
||||||
|
|
||||||
cd core
|
cd core
|
||||||
make dist VERSION="$VERSION" COMMIT="$COMMIT"
|
make dist VERSION="$VERSION" COMMIT="$COMMIT"
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ in
|
|||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
|
||||||
|
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
|
||||||
|
# U2F and fingerprint are handled separately by DMS — do not add pam_u2f or pam_fprintd here.
|
||||||
|
# security.pam.services.dankshell = {
|
||||||
|
# # Example: add faillock
|
||||||
|
# faillock.enable = true;
|
||||||
|
# };
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = lib.mkDefault true;
|
enable = lib.mkDefault true;
|
||||||
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}: let
|
||||||
let
|
|
||||||
cfg = config.programs.dank-material-shell;
|
cfg = config.programs.dank-material-shell;
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
imports = [
|
imports = [
|
||||||
./dms-rename.nix
|
./dms-rename.nix
|
||||||
];
|
];
|
||||||
@@ -16,9 +14,11 @@ in
|
|||||||
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
||||||
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
||||||
includes = {
|
includes = {
|
||||||
enable = (lib.mkEnableOption "includes for niri-flake") // {
|
enable =
|
||||||
default = true;
|
(lib.mkEnableOption "includes for niri-flake")
|
||||||
};
|
// {
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
override = lib.mkOption {
|
override = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = ''
|
description = ''
|
||||||
@@ -44,8 +44,10 @@ in
|
|||||||
"alttab"
|
"alttab"
|
||||||
"binds"
|
"binds"
|
||||||
"colors"
|
"colors"
|
||||||
|
"cursor"
|
||||||
"layout"
|
"layout"
|
||||||
"outputs"
|
"outputs"
|
||||||
|
"windowrules"
|
||||||
"wpblur"
|
"wpblur"
|
||||||
];
|
];
|
||||||
example = [
|
example = [
|
||||||
@@ -70,24 +72,21 @@ in
|
|||||||
let
|
let
|
||||||
cfg' = cfg.niri.includes;
|
cfg' = cfg.niri.includes;
|
||||||
|
|
||||||
withOriginalConfig =
|
withOriginalConfig = dmsFiles:
|
||||||
dmsFiles:
|
if cfg'.override
|
||||||
if cfg'.override then
|
then [cfg'.originalFileName] ++ dmsFiles
|
||||||
[ cfg'.originalFileName ] ++ dmsFiles
|
else dmsFiles ++ [cfg'.originalFileName];
|
||||||
else
|
|
||||||
dmsFiles ++ [ cfg'.originalFileName ];
|
|
||||||
|
|
||||||
fixes = map (fix: "\n${fix}") (
|
fixes = map (fix: "\n${fix}") (
|
||||||
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
||||||
# kdl
|
# kdl
|
||||||
''
|
''
|
||||||
// Border fix
|
// Border fix
|
||||||
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
||||||
layout { border { on; }; }
|
layout { border { on; }; }
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
||||||
niri-config-dms = {
|
niri-config-dms = {
|
||||||
target = "niri/config.kdl";
|
target = "niri/config.kdl";
|
||||||
@@ -104,11 +103,9 @@ in
|
|||||||
|
|
||||||
programs.niri.settings = lib.mkMerge [
|
programs.niri.settings = lib.mkMerge [
|
||||||
(lib.mkIf cfg.niri.enableKeybinds {
|
(lib.mkIf cfg.niri.enableKeybinds {
|
||||||
binds =
|
binds = with config.lib.niri.actions; let
|
||||||
with config.lib.niri.actions;
|
dms-ipc = spawn "dms" "ipc";
|
||||||
let
|
in
|
||||||
dms-ipc = spawn "dms" "ipc";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
"Mod+Space" = {
|
"Mod+Space" = {
|
||||||
action = dms-ipc "spotlight" "toggle";
|
action = dms-ipc "spotlight" "toggle";
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ in
|
|||||||
|
|
||||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
services.power-profiles-daemon.enable = lib.mkDefault true;
|
||||||
services.accounts-daemon.enable = lib.mkDefault true;
|
services.accounts-daemon.enable = lib.mkDefault true;
|
||||||
|
services.geoclue2.enable = lib.mkDefault true;
|
||||||
security.polkit.enable = lib.mkDefault true;
|
security.polkit.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,10 @@ mkdir -p $HOME $GOCACHE $GOMODCACHE
|
|||||||
# OBS has no network access, so use local toolchain only
|
# OBS has no network access, so use local toolchain only
|
||||||
export GOTOOLCHAIN=local
|
export GOTOOLCHAIN=local
|
||||||
|
|
||||||
# Patch go.mod to use base Go version (e.g., go 1.24 instead of go 1.24.6)
|
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
||||||
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
|
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
|
||||||
|
|
||||||
# Extract version info for embedding in binary
|
# Extract version info for embedding in binary
|
||||||
VERSION="%{version}"
|
VERSION="%{version}"
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ if [[ ! -d "distro/debian" ]]; then
|
|||||||
echo "Error: Run this script from the repository root"
|
echo "Error: Run this script from the repository root"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
|
||||||
|
osc_retry() {
|
||||||
|
local max=3 attempt=1
|
||||||
|
while true; do
|
||||||
|
if osc "$@"; then return 0; fi
|
||||||
|
((attempt >= max)) && return 1
|
||||||
|
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
|
||||||
|
sleep $((5*attempt))
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# $1 = PROJECT
|
# $1 = PROJECT
|
||||||
# $2 = PACKAGE
|
# $2 = PACKAGE
|
||||||
@@ -309,8 +322,23 @@ mkdir -p "$OBS_BASE"
|
|||||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||||
cd "$OBS_BASE"
|
cd "$OBS_BASE"
|
||||||
osc co "$OBS_PROJECT/$PACKAGE"
|
CHECKOUT_OK=false
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if osc co "$OBS_PROJECT/$PACKAGE"; then
|
||||||
|
CHECKOUT_OK=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ $attempt -lt 3 ]]; then
|
||||||
|
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
|
||||||
|
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
|
||||||
|
sleep $((5*attempt))
|
||||||
|
fi
|
||||||
|
done
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
||||||
|
echo "Error: Checkout failed after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
||||||
@@ -419,6 +447,9 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
|||||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
|
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
||||||
|
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
||||||
|
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
||||||
@@ -813,6 +844,9 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
|||||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
|
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
||||||
|
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
||||||
|
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1058,7 +1092,7 @@ fi
|
|||||||
|
|
||||||
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
||||||
echo "==> Updating working copy"
|
echo "==> Updating working copy"
|
||||||
if ! osc up 2>/dev/null; then
|
if ! osc_retry up 2>/dev/null; then
|
||||||
echo "Error: Failed to update working copy"
|
echo "Error: Failed to update working copy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1139,7 +1173,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
|||||||
else
|
else
|
||||||
echo "==> Committing to OBS"
|
echo "==> Committing to OBS"
|
||||||
set +e
|
set +e
|
||||||
osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
||||||
COMMIT_EXIT=${PIPESTATUS[0]}
|
COMMIT_EXIT=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user