mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb5afcdc40 | |||
| dd2a3f3efc | |||
| 161fa2dba5 | |||
| f5f50f7add | |||
| 372cf2f566 | |||
| b70acbc283 | |||
| 571c1158bf | |||
| ac03a2e4b2 | |||
| 07460dc3b7 | |||
| 7412fee590 | |||
| 85c2954958 | |||
| 7c9e805cbe | |||
| 6926470b04 | |||
| 713ba1efbb | |||
| 1919ca7243 | |||
| bcf41ed5ca | |||
| 5033bdc630 | |||
| b8bfaf9a26 | |||
| da45714c54 | |||
| 0c2d00b79c | |||
| c10b42f599 | |||
| 4c617cf022 | |||
| e75b95b854 | |||
| 6b15670918 | |||
| c52b9e19a1 | |||
| 7a3444bd30 | |||
| a733d760e4 | |||
| 1b33079e39 | |||
| 1cf0dd1031 | |||
| 8d49a5cbfc | |||
| f5928b09d3 | |||
| 38373aa5f2 | |||
| 665680e15e | |||
| 210607cfbc | |||
| 69fca14611 | |||
| 10a235e686 | |||
| 253cc7f8a3 | |||
| a63ad99684 | |||
| c44c032879 | |||
| dc1ce55971 | |||
| 9f65882a12 | |||
| 97bf83cce6 | |||
| 96bf0162d6 | |||
| 73b833731a | |||
| 84522aeaad | |||
| faf1a277d2 | |||
| 60515736e6 | |||
| 1715e2eab7 | |||
| 4e14cf5cce | |||
| a644c93b1b | |||
| f9428a1009 | |||
| b4b51785e5 | |||
| 0a97df6d49 | |||
| 352ba77677 | |||
| d320035d97 | |||
| 8d262a9555 | |||
| 9bfa8310d2 | |||
| 088ed806ae | |||
| 07d2c94676 | |||
| 0bc1b7a3c2 | |||
| c5987b28c0 | |||
| 18901c7cde | |||
| 519a8357a1 | |||
| 799773c62b | |||
| 247a674c79 | |||
| 72b598057c | |||
| 8180e30e8e | |||
| dd9851b4f0 | |||
| 056576fbe7 | |||
| d28a5bdf7f | |||
| f62ea119f7 | |||
| a1f9b98727 | |||
| b9c8914d46 | |||
| 8697840d46 | |||
| 1f64bb8031 | |||
| eea7d12c0b | |||
| 85173126f4 | |||
| 222187d8a6 | |||
| bef3f65f63 | |||
| bff83fe563 | |||
| cbf00d133a | |||
| 347f06b758 | |||
| 9070903512 | |||
| e9d030f6d8 | |||
| fbf9e6d1b9 | |||
| e803812344 | |||
| 9a64f2acf0 | |||
| c647eafadc | |||
| 720ec07d13 | |||
| 4b4334e611 | |||
| b69a96e80b | |||
| 1e6a73fd60 | |||
| 60b6280750 | |||
| 9e079f8a4b | |||
| 62c2e858ef | |||
| 78357d45bb | |||
| 3ff9564c9b | |||
| b0989cecad | |||
| 47be6a1033 | |||
| 31b415b086 | |||
| 7156e1e299 | |||
| c72c9bfb08 | |||
| 73c75fcc2c | |||
| 2ff42eba41 | |||
| 9f13465cd7 | |||
| 366a98e0cc | |||
| 31aeb8dc4b | |||
| c4e7f3d62f | |||
| a1d13f276a | |||
| dbf132d633 | |||
| 59451890f1 | |||
| e633c9e039 | |||
| 6c1fff2df1 | |||
| 3891d125d1 | |||
| 997011e008 | |||
| 2504396435 | |||
| d206723b36 | |||
| a0ec3d59b8 | |||
| 17ef08aa58 | |||
| 57279d1c53 | |||
| 8b003ac9cd | |||
| 0ea10b0ad2 | |||
| 2db4c9daa0 | |||
| 363964e90b | |||
| a7b49eba70 | |||
| 4ae334f60f | |||
| 86c0064ff9 | |||
| 5a6b52f07f | |||
| 5aaa56853f | |||
| 35913c22f5 | |||
| d7b560573c | |||
| 02a274ebe2 | |||
| fc7b61c20b | |||
| 5880043f56 | |||
| fee3b7f2a7 | |||
| c0b0339fca | |||
| 26c1e62204 | |||
| 7b2d4dbe30 | |||
| 78c5d46c6b | |||
| 3fb85df504 | |||
| 227dd24726 | |||
| ae6a656899 | |||
| a4055e0f01 | |||
| 6d98c229ef | |||
| 71d93ad85e | |||
| 4ec21fcd3d | |||
| 0a2fe03fee | |||
| 4f4745609b | |||
| a69cd515fb | |||
| 06c4b97a6b | |||
| a6cf71a190 | |||
| 21750156dc | |||
| f9b737f543 | |||
| 246b59f3b9 | |||
| dcda81ea64 | |||
| 9909b665cd | |||
| 4bcd786be3 | |||
| 64c9222000 | |||
| 12acf2dd51 | |||
| fea97b4aad | |||
| c6d398eeac | |||
| 7a74be83d7 | |||
| 67a6427418 | |||
| 18b20d3225 | |||
| 8a76885fb6 | |||
| 69b1e61ab7 | |||
| 3f24cf37ca | |||
| 01218f34cb | |||
| 9da58d8296 | |||
| af0038e634 | |||
| 05c312b9eb | |||
| 89d5c958c4 | |||
| e4d86ad595 | |||
| 532b54a028 | |||
| 504d027c3f | |||
| e8f95f4533 | |||
| b83256c83a | |||
| 8e2cd21be8 | |||
| c5413608da | |||
| 586bcad442 | |||
| 3b3d10f730 | |||
| 4834891b36 | |||
| f60e65aecb | |||
| 01387b0123 | |||
| 1476658c23 | |||
| 7861c6e316 | |||
| d2247d7b24 | |||
| 2ff78d4a02 | |||
| 785243ce5f | |||
| 0e1b868384 | |||
| 2b08e800e8 | |||
| 74e4f8ea1e | |||
| 9c58569b4c | |||
| 29de677e00 | |||
| fae4944845 | |||
| 07a0ac4b7d | |||
| b2d8f4d73b | |||
| fe58c45233 | |||
| 3ea4e389eb | |||
| 7276f295fc | |||
| 93ed96a789 | |||
| bea325e94c | |||
| 2f8f1c30ad | |||
| f859a14173 | |||
| 153f39da48 | |||
| e4accdd1c7 | |||
| a2c89e0a8c | |||
| e282831c2e | |||
| 5c5ff6195a | |||
| c4bbf54679 | |||
| 98acafb4b8 | |||
| da20681fc0 | |||
| b38cb961b2 | |||
| 7a0bb07518 | |||
| 403e3e90a2 | |||
| 50b91f14b6 | |||
| b3df47fce0 | |||
| 09bd65d746 | |||
| 020d56ab7f | |||
| f3bee65da9 | |||
| b14b0946e2 | |||
| ca44205f1c | |||
| 2d39e8fd2a | |||
| 6d4df6e927 | |||
| b8ab86e6c0 | |||
| 837329a6d8 | |||
| 8c6c2ffd23 | |||
| ad3c8b6755 | |||
| 03a8e1e0d5 | |||
| 4d4d3c20a1 | |||
| cef16d6bc9 | |||
| aafaad1791 | |||
| 7906fdc2b0 | |||
| 397650ca52 | |||
| 826207006a | |||
| 58c2fcd31c | |||
| b2a2b425ec | |||
| 942c9c9609 | |||
| 46d6e1cff3 | |||
| a4137c57c1 | |||
| 1ad8b627f1 | |||
| 58a02ce290 | |||
| 8e1ad1a2be | |||
| 68cd7ab32c | |||
| f649ce9a8e | |||
| c4df242f07 | |||
| 26846c8d55 | |||
| 31b44a667c | |||
| 4f3b73ee21 | |||
| 4cfae91f02 | |||
| 8d947a6e95 | |||
| 1e84d4252c | |||
| 76072e1d4c | |||
| 6408dce4a9 | |||
| 0b2e1cca38 | |||
| c1bfd8c0b7 | |||
| 90ffa5833b | |||
| 169c669286 | |||
| f8350deafc | |||
| 0286a1b80b | |||
| 7c3e6c1f02 | |||
| d2d72db3c9 | |||
| f81f861408 | |||
| af494543f5 | |||
| db4de55338 | |||
| 37ecbbbbde | |||
| d6a6d2a438 | |||
| bf1c6eec74 | |||
| 0ddae80584 | |||
| 5c96c03bfa | |||
| dfe36e47d8 | |||
| 63e1b75e57 | |||
| 29efdd8598 | |||
| 34d03cf11b | |||
| c339389d44 | |||
| af5f6eb656 | |||
| a6d28e2553 | |||
| 6213267908 | |||
| d084114149 | |||
| f6d99eca0d | |||
| 722eb3289e | |||
| b7f2bdcb2d | |||
| 11c20db6e6 | |||
| 8a4e3f8bb1 | |||
| bc8fe97c13 | |||
| 47262155aa |
@@ -20,7 +20,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
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@v6
|
uses: actions/setup-go@v5
|
||||||
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@v5
|
uses: actions/upload-artifact@v4
|
||||||
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@v5
|
uses: actions/upload-artifact@v4
|
||||||
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@v6
|
# uses: actions/checkout@v4
|
||||||
# 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@v6
|
uses: actions/checkout@v4
|
||||||
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@v6
|
uses: actions/setup-go@v5
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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@v5
|
uses: actions/upload-artifact@v4
|
||||||
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 }}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
|
- dms-greeter
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -119,8 +119,9 @@ 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 stable package and build list of those needing updates
|
# Check each 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
|
||||||
@@ -139,7 +140,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 "✓ Both packages up to date"
|
echo "✓ All packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -195,13 +196,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
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: |
|
||||||
@@ -247,7 +245,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
- name: Update dms-git spec version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
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)
|
||||||
@@ -268,7 +266,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')
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
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)
|
||||||
@@ -347,7 +345,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -391,7 +389,7 @@ jobs:
|
|||||||
UPLOADED_PACKAGES=()
|
UPLOADED_PACKAGES=()
|
||||||
SKIPPED_PACKAGES=()
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" 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 ""
|
||||||
|
|||||||
@@ -4,15 +4,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to upload"
|
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||||
required: true
|
required: false
|
||||||
type: choice
|
default: "dms-git"
|
||||||
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
|
||||||
@@ -31,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -145,7 +139,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Fallback
|
# Fallback
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -157,12 +151,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
@@ -215,7 +209,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 dms-greeter" from "all" check)
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" 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
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
|
||||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
git add flake.nix
|
git add flake.nix
|
||||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
git pull --rebase origin master
|
git pull --rebase origin ${{ github.ref_name }}
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||||
else
|
else
|
||||||
echo "No changes to flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+1
-3
@@ -86,9 +86,7 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
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`.
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
6. Make your changes, test, and open a pull request.
|
|
||||||
|
|
||||||
### I18n/Localization
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
@@ -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 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
|
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ 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)..."
|
||||||
@@ -133,7 +130,6 @@ 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,12 +28,6 @@ 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"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"blocked_commands": [
|
"blocked_commands": [
|
||||||
"greeter install",
|
"greeter install",
|
||||||
"greeter enable",
|
"greeter enable",
|
||||||
"greeter sync",
|
|
||||||
"greeter uninstall",
|
"greeter uninstall",
|
||||||
"setup"
|
"setup"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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,6 +236,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
PreRunE: findConfig,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -526,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
randrCmd,
|
||||||
|
blurCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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"
|
||||||
@@ -82,7 +83,7 @@ func (ds *DoctorStatus) OKCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
quickshellVersionRegex = regexp.MustCompile(`(?i)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+)`)
|
||||||
@@ -90,6 +91,7 @@ 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{
|
||||||
@@ -468,6 +470,7 @@ 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
|
||||||
@@ -500,7 +503,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, or Wayfire",
|
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -509,9 +512,24 @@ 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 {
|
||||||
@@ -535,6 +553,8 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -553,6 +573,7 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
|||||||
qmlContent := `
|
qmlContent := `
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -561,6 +582,7 @@ 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
|
||||||
@@ -578,16 +600,18 @@ ShellRoot {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var testItem = Qt.createQmlObject(
|
var testItem = Qt.createQmlObject(
|
||||||
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
'import Quickshell; 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) {}
|
||||||
|
|
||||||
@@ -596,6 +620,8 @@ 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)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,6 +642,7 @@ 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
|
||||||
@@ -820,10 +847,14 @@ 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"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
return !utils.CommandExists(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
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, or alacritty", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
networkResult, err := network.DetectNetworkStack()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -109,16 +111,37 @@ func updateArchLinux() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packageName string
|
var packageName string
|
||||||
if isArchPackageInstalled("dms-shell-bin") {
|
var isAUR bool
|
||||||
packageName = "dms-shell-bin"
|
if isArchPackageInstalled("dms-shell") {
|
||||||
|
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: Neither dms-shell-bin nor dms-shell-git package found.")
|
fmt.Println("Info: No dms-shell 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: pacman -S %s\n", packageName)
|
||||||
|
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("dms successfully updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
@@ -454,11 +477,7 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
fmt.Printf("Installing to %s...\n", currentPath)
|
fmt.Printf("Installing to %s...\n", currentPath)
|
||||||
|
|
||||||
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
||||||
replaceCmd.Stdin = os.Stdin
|
|
||||||
replaceCmd.Stdout = os.Stdout
|
|
||||||
replaceCmd.Stderr = os.Stderr
|
|
||||||
if err := replaceCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to replace binary: %w", err)
|
return fmt.Errorf("failed to replace binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -13,6 +14,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"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"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"
|
||||||
@@ -29,7 +31,7 @@ var greeterInstallCmd = &cobra.Command{
|
|||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -54,7 +56,7 @@ var greeterSyncCmd = &cobra.Command{
|
|||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync DMS theme and settings with greeter",
|
Short: "Sync DMS theme and settings with greeter",
|
||||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
auth, _ := cmd.Flags().GetBool("auth")
|
auth, _ := cmd.Flags().GetBool("auth")
|
||||||
@@ -83,7 +85,7 @@ var greeterEnableCmd = &cobra.Command{
|
|||||||
Use: "enable",
|
Use: "enable",
|
||||||
Short: "Enable DMS greeter in greetd config",
|
Short: "Enable DMS greeter in greetd config",
|
||||||
Long: "Configure greetd to use DMS as the greeter",
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -119,7 +121,7 @@ var greeterUninstallCmd = &cobra.Command{
|
|||||||
Use: "uninstall",
|
Use: "uninstall",
|
||||||
Short: "Remove DMS greeter configuration and restore previous display manager",
|
Short: "Remove DMS greeter configuration and restore previous display manager",
|
||||||
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -289,10 +291,7 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nDisabling greetd...")
|
fmt.Println("\nDisabling greetd...")
|
||||||
disableCmd := exec.Command("sudo", "systemctl", "disable", "greetd")
|
if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil {
|
||||||
disableCmd.Stdout = os.Stdout
|
|
||||||
disableCmd.Stderr = os.Stderr
|
|
||||||
if err := disableCmd.Run(); err != nil {
|
|
||||||
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✓ greetd disabled")
|
fmt.Println(" ✓ greetd disabled")
|
||||||
@@ -358,10 +357,10 @@ func restorePreDMSGreetdConfig(sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||||
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCommand(sudoPassword, "chmod", "644", configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
||||||
@@ -389,21 +388,14 @@ command = "agreety --cmd /bin/bash"
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||||
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
||||||
}
|
}
|
||||||
_ = runSudoCommand(sudoPassword, "chmod", "644", configPath)
|
_ = privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath)
|
||||||
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSudoCommand(_ string, command string, args ...string) error {
|
|
||||||
cmd := exec.Command("sudo", append([]string{command}, args...)...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
||||||
func suggestDisplayManagerRestore(nonInteractive bool) {
|
func suggestDisplayManagerRestore(nonInteractive bool) {
|
||||||
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
@@ -422,10 +414,7 @@ func suggestDisplayManagerRestore(nonInteractive bool) {
|
|||||||
|
|
||||||
enableDM := func(dm string) {
|
enableDM := func(dm string) {
|
||||||
fmt.Printf(" Enabling %s...\n", dm)
|
fmt.Printf(" Enabling %s...\n", dm)
|
||||||
cmd := exec.Command("sudo", "systemctl", "enable", "--force", dm)
|
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil {
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
||||||
@@ -624,10 +613,7 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
|
|
||||||
if response != "n" && response != "no" {
|
if response != "n" && response != "no" {
|
||||||
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
if err := privesc.Run(context.Background(), "", "usermod", "-aG", greeterGroup, currentUser.Username); err != nil {
|
||||||
addUserCmd.Stdout = os.Stdout
|
|
||||||
addUserCmd.Stderr = os.Stderr
|
|
||||||
if err := addUserCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||||
@@ -847,22 +833,19 @@ func disableDisplayManager(dmName string) (bool, error) {
|
|||||||
actionTaken := false
|
actionTaken := false
|
||||||
|
|
||||||
if state.NeedsDisable {
|
if state.NeedsDisable {
|
||||||
var disableCmd *exec.Cmd
|
var action, actionVerb string
|
||||||
var actionVerb string
|
switch state.EnabledState {
|
||||||
|
case "static":
|
||||||
if state.EnabledState == "static" {
|
|
||||||
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
||||||
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
|
action = "mask"
|
||||||
actionVerb = "masked"
|
actionVerb = "masked"
|
||||||
} else {
|
default:
|
||||||
fmt.Printf(" Disabling %s...\n", dmName)
|
fmt.Printf(" Disabling %s...\n", dmName)
|
||||||
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
|
action = "disable"
|
||||||
actionVerb = "disabled"
|
actionVerb = "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
disableCmd.Stdout = os.Stdout
|
if err := privesc.Run(context.Background(), "", "systemctl", action, dmName); err != nil {
|
||||||
disableCmd.Stderr = os.Stderr
|
|
||||||
if err := disableCmd.Run(); err != nil {
|
|
||||||
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,10 +886,7 @@ func ensureGreetdEnabled() error {
|
|||||||
|
|
||||||
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||||
fmt.Println(" Unmasking greetd...")
|
fmt.Println(" Unmasking greetd...")
|
||||||
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
|
if err := privesc.Run(context.Background(), "", "systemctl", "unmask", "greetd"); err != nil {
|
||||||
unmaskCmd.Stdout = os.Stdout
|
|
||||||
unmaskCmd.Stderr = os.Stderr
|
|
||||||
if err := unmaskCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println(" ✓ Unmasked greetd")
|
fmt.Println(" ✓ Unmasked greetd")
|
||||||
@@ -918,10 +898,7 @@ func ensureGreetdEnabled() error {
|
|||||||
fmt.Println(" Enabling greetd service...")
|
fmt.Println(" Enabling greetd service...")
|
||||||
}
|
}
|
||||||
|
|
||||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||||
enableCmd.Stdout = os.Stdout
|
|
||||||
enableCmd.Stderr = os.Stderr
|
|
||||||
if err := enableCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,10 +928,7 @@ func ensureGraphicalTarget() error {
|
|||||||
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
||||||
if currentTargetStr != "graphical.target" {
|
if currentTargetStr != "graphical.target" {
|
||||||
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
||||||
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
|
if err := privesc.Run(context.Background(), "", "systemctl", "set-default", "graphical.target"); err != nil {
|
||||||
setDefaultCmd.Stdout = os.Stdout
|
|
||||||
setDefaultCmd.Stderr = os.Stderr
|
|
||||||
if err := setDefaultCmd.Run(); err != nil {
|
|
||||||
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
||||||
fmt.Println(" Greeter may not start on boot. Run manually:")
|
fmt.Println(" Greeter may not start on boot. Run manually:")
|
||||||
fmt.Println(" sudo systemctl set-default graphical.target")
|
fmt.Println(" sudo systemctl set-default graphical.target")
|
||||||
@@ -1491,6 +1465,19 @@ func checkGreeterStatus() error {
|
|||||||
}
|
}
|
||||||
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||||
|
requiredSubdirs := []string{".local/state", ".local/share", ".cache"}
|
||||||
|
missingSubdirs := false
|
||||||
|
for _, sub := range requiredSubdirs {
|
||||||
|
subPath := filepath.Join(cacheDir, sub)
|
||||||
|
if _, err := os.Stat(subPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf(" ⚠ Missing required subdir: %s\n", subPath)
|
||||||
|
missingSubdirs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missingSubdirs {
|
||||||
|
fmt.Println(" Run 'dms greeter sync' to initialize the cache directory structure.")
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||||
fmt.Printf(" %s\n", packageInstallHint())
|
fmt.Printf(" %s\n", packageInstallHint())
|
||||||
@@ -1498,6 +1485,20 @@ func checkGreeterStatus() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguration Symlinks:")
|
fmt.Println("\nConfiguration Symlinks:")
|
||||||
|
colorSyncInfo, colorSyncErr := greeter.ResolveGreeterColorSyncInfo(homeDir)
|
||||||
|
if colorSyncErr != nil {
|
||||||
|
fmt.Printf(" ✗ Failed to resolve expected greeter color source: %v\n", colorSyncErr)
|
||||||
|
allGood = false
|
||||||
|
colorSyncInfo = greeter.GreeterColorSyncInfo{
|
||||||
|
SourcePath: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colorThemeDesc := "Color theme"
|
||||||
|
if colorSyncInfo.UsesDynamicWallpaperOverride {
|
||||||
|
colorThemeDesc = "Color theme (greeter wallpaper override)"
|
||||||
|
}
|
||||||
|
|
||||||
symlinks := []struct {
|
symlinks := []struct {
|
||||||
source string
|
source string
|
||||||
target string
|
target string
|
||||||
@@ -1514,9 +1515,9 @@ func checkGreeterStatus() error {
|
|||||||
desc: "Session state",
|
desc: "Session state",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
source: colorSyncInfo.SourcePath,
|
||||||
target: filepath.Join(cacheDir, "colors.json"),
|
target: filepath.Join(cacheDir, "colors.json"),
|
||||||
desc: "Color theme",
|
desc: colorThemeDesc,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1558,6 +1559,10 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if colorSyncInfo.UsesDynamicWallpaperOverride {
|
||||||
|
fmt.Printf(" ℹ Dynamic theme uses greeter override colors from %s\n", colorSyncInfo.SourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nGreeter Wallpaper Override:")
|
fmt.Println("\nGreeter Wallpaper Override:")
|
||||||
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
|
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"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"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -19,7 +21,7 @@ var setupCmd = &cobra.Command{
|
|||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
PersistentPreRunE: requireMutableSystemCommand,
|
PersistentPreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
@@ -267,6 +269,8 @@ func runSetupDmsConfig(name string) error {
|
|||||||
func runSetup() error {
|
func runSetup() error {
|
||||||
fmt.Println("=== DMS Configuration Setup ===")
|
fmt.Println("=== DMS Configuration Setup ===")
|
||||||
|
|
||||||
|
ensureInputGroup()
|
||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := promptSystemd()
|
useSystemd := promptSystemd()
|
||||||
@@ -340,6 +344,37 @@ func runSetup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
|
func ensureInputGroup() {
|
||||||
|
if !utils.HasGroup("input") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentUser := os.Getenv("USER")
|
||||||
|
if currentUser == "" {
|
||||||
|
currentUser = os.Getenv("LOGNAME")
|
||||||
|
}
|
||||||
|
if currentUser == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := execGroups(currentUser)
|
||||||
|
if err == nil && strings.Contains(out, "input") {
|
||||||
|
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Adding user to input group for Caps Lock OSD support...")
|
||||||
|
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
|
||||||
|
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execGroups(user string) (string, error) {
|
||||||
|
out, err := exec.Command("groups", user).Output()
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
func promptCompositor() (deps.WindowManager, bool) {
|
func promptCompositor() (deps.WindowManager, bool) {
|
||||||
fmt.Println("Select compositor:")
|
fmt.Println("Select compositor:")
|
||||||
fmt.Println("1) Niri")
|
fmt.Println("1) Niri")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@ func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
|||||||
immutablePolicy = immutableCommandPolicy{
|
immutablePolicy = immutableCommandPolicy{
|
||||||
ImmutableSystem: detectedImmutable,
|
ImmutableSystem: detectedImmutable,
|
||||||
ImmutableReason: reason,
|
ImmutableReason: reason,
|
||||||
BlockedCommands: []string{"greeter install", "greeter enable", "greeter sync", "setup"},
|
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
||||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,3 +270,16 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
|||||||
|
|
||||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preRunPrivileged combines the immutable-system check with a privesc tool
|
||||||
|
// selection prompt (shown only when multiple tools are available and the
|
||||||
|
// $DMS_PRIVESC env var isn't set).
|
||||||
|
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +29,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
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,6 +5,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +26,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+51
-3
@@ -616,6 +616,43 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFirstDMSPID() (int, bool) {
|
||||||
|
dir := getRuntimeDir()
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if proc.Signal(syscall.Signal(0)) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -627,10 +664,21 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
switch pid, ok := getFirstDMSPID(); {
|
||||||
|
case ok:
|
||||||
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
|
default:
|
||||||
|
if err := findConfig(nil, nil); err != nil {
|
||||||
|
log.Fatalf("Error finding config: %v", err)
|
||||||
|
}
|
||||||
|
// ! TODO - remove check when QS 0.3 is released
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
|
||||||
cmdArgs = append(cmdArgs, args...)
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ 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()
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ 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
|
||||||
@@ -34,19 +32,15 @@ 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
@@ -58,8 +58,6 @@ 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=
|
||||||
@@ -77,8 +75,6 @@ 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=
|
||||||
@@ -119,8 +115,6 @@ 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=
|
||||||
@@ -148,12 +142,6 @@ 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=
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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,7 +1,6 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,100 +12,166 @@ 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 CopyReader(bytes.NewReader(data), mimeType, false, false)
|
return copyForkCached(data, mimeType, 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 copyServeWithWriter(func(writer io.Writer) error {
|
return serveClipboard(data, mimeType, pasteOnce)
|
||||||
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 CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
return copyForkCached(data, mimeType, 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 {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServeReader(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
cmd := exec.Command(os.Args[0])
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
if stdinSource, ok := data.(*os.File); ok {
|
envServe+"=1",
|
||||||
cmd.Stdin = stdinSource
|
envMime+"="+mimeType,
|
||||||
return cmd.Start()
|
)
|
||||||
|
if pasteOnce {
|
||||||
|
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||||
}
|
}
|
||||||
|
cmd.Env = append(cmd.Env, extra...)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
func waitReady(cmd *exec.Cmd) error {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
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)
|
||||||
}
|
}
|
||||||
|
var buf [1]byte
|
||||||
if _, err := io.Copy(stdin, data); err != nil {
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
stdin.Close()
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
}
|
||||||
if err := stdin.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
cachedData, err := createClipboardCacheFile()
|
cacheFile, err := createClipboardCacheFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
return fmt.Errorf("create cache file: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(cachedData.Name())
|
cachePath := cacheFile.Name()
|
||||||
|
|
||||||
if _, err := io.Copy(cachedData, data); err != nil {
|
if _, err := cacheFile.Write(data); err != nil {
|
||||||
return fmt.Errorf("cache clipboard data: %w", err)
|
cacheFile.Close()
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("write cache file: %w", err)
|
||||||
}
|
}
|
||||||
if err := cachedData.Close(); err != nil {
|
if err := cacheFile.Close(); err != nil {
|
||||||
return fmt.Errorf("close temp cache file: %w", err)
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("close cache file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||||
cachedFile, err := os.Open(cachedData.Name())
|
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) {
|
||||||
|
case *os.File:
|
||||||
|
cmd.Stdin = src
|
||||||
|
return waitReady(cmd)
|
||||||
|
|
||||||
|
default:
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open temp cache file: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
defer cachedFile.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
stdout, err := cmd.StdoutPipe()
|
||||||
return fmt.Errorf("write clipboard data: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
|
}
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, mimeType, pasteOnce)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalReady() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte{1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
@@ -129,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
func serveClipboard(data []byte, 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)
|
||||||
@@ -171,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
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")
|
||||||
}
|
}
|
||||||
@@ -215,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
|
|
||||||
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) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := writeTo(file); err != nil {
|
_, _ = file.Write(data)
|
||||||
select {
|
|
||||||
case sendErr <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -242,13 +299,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
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
|
||||||
@@ -502,12 +558,10 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -535,12 +589,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) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
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%
|
bind = SUPER CTRL, F, resizeactive, exact 100% 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,6 +94,7 @@ 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,6 +224,7 @@ 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$"#
|
||||||
@@ -252,6 +253,7 @@ window-rule {
|
|||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
|
match app-id=r#"com.danklinux.dms$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
+143
-78
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -135,6 +136,42 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
||||||
|
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
||||||
|
data, err := os.ReadFile(srcinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
var pkg string
|
||||||
|
var target *[]string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "makedepends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "makedepends = ")
|
||||||
|
target = &makedeps
|
||||||
|
case strings.HasPrefix(line, "depends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "depends = ")
|
||||||
|
target = &deps
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
||||||
|
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
||||||
|
pkg = pkg[:idx]
|
||||||
|
}
|
||||||
|
pkg = strings.TrimSpace(pkg)
|
||||||
|
if pkg != "" {
|
||||||
|
*target = append(*target, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps, makedeps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
||||||
|
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -206,11 +243,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
|||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.packageInstalled("dms-shell-bin") {
|
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||||
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 {
|
||||||
@@ -260,7 +293,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
|
|||||||
LogOutput: "Installing base-devel development tools",
|
LogOutput: "Installing base-devel development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||||
}
|
}
|
||||||
@@ -292,6 +325,13 @@ 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{
|
||||||
@@ -409,6 +449,37 @@ 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 := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||||
|
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: "Building quickshell-git before system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
|
||||||
|
}
|
||||||
|
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
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
|
||||||
@@ -417,6 +488,9 @@ 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{
|
||||||
@@ -428,7 +502,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +578,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" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
@@ -524,6 +598,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
|
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
||||||
|
if visited[pkg] {
|
||||||
|
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
visited[pkg] = true
|
||||||
|
|
||||||
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)
|
||||||
@@ -575,7 +659,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
depsToRemove := []string{
|
depsToRemove := []string{
|
||||||
"depends = quickshell",
|
"depends = quickshell",
|
||||||
@@ -598,51 +682,65 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Classifying dependencies as system or AUR",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies from .SRCINFO
|
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
||||||
depFilter := ""
|
if err != nil {
|
||||||
if pkg == "dms-shell-git" {
|
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
||||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
seen := make(map[string]bool)
|
||||||
fmt.Sprintf(`
|
var systemPkgs []string
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
var aurPkgs []string
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, depFilter, sudoPassword))
|
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
for _, dep := range append(runtimeDeps, makeDeps...) {
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
if seen[dep] || a.packageInstalled(dep) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dep] = true
|
||||||
|
if a.isInSystemRepo(dep) {
|
||||||
|
systemPkgs = append(systemPkgs, dep)
|
||||||
|
} else {
|
||||||
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
if len(systemPkgs) > 0 {
|
||||||
fmt.Sprintf(`
|
progressChan <- InstallProgressMsg{
|
||||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
Phase: PhaseAURPackages,
|
||||||
if [ ! -z "$makedeps" ]; then
|
Progress: startProgress + 0.32*(endProgress-startProgress),
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
||||||
fi
|
IsComplete: false,
|
||||||
`, srcinfoPath, sudoPassword))
|
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
||||||
|
}
|
||||||
|
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
for _, aurDep := range aurPkgs {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
||||||
|
}
|
||||||
|
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
||||||
|
startProgress+0.35*(endProgress-startProgress),
|
||||||
|
startProgress+0.39*(endProgress-startProgress),
|
||||||
|
visited,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,42 +769,9 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
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
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||||
// For DMS split packages, install base package
|
files = matches
|
||||||
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)
|
||||||
@@ -715,7 +780,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||||
installArgs = append(installArgs, files...)
|
installArgs = append(installArgs, files...)
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||||
|
|
||||||
fileNames := make([]string, len(files))
|
fileNames := make([]string, len(files))
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,27 +56,6 @@ func (b *BaseDistribution) logError(message string, err error) {
|
|||||||
b.log(errorMsg)
|
b.log(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
|
||||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
|
||||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
|
||||||
func escapeSingleQuotes(s string) string {
|
|
||||||
return strings.ReplaceAll(s, "'", "'\\''")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
|
||||||
// This helper escapes special characters in the password to prevent shell injection
|
|
||||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
|
||||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
|
||||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
|
||||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
|
||||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
|
||||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists(name) {
|
if b.commandExists(name) {
|
||||||
@@ -710,7 +690,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install to /usr/local/bin
|
// Install to /usr/local/bin
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -113,6 +113,17 @@ func containsString(values []string, target string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func debianRepoArchitecture(arch string) string {
|
||||||
|
switch arch {
|
||||||
|
case "amd64", "x86_64":
|
||||||
|
return "amd64"
|
||||||
|
case "arm64", "aarch64":
|
||||||
|
return "arm64"
|
||||||
|
default:
|
||||||
|
return arch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -181,7 +192,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -198,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -214,7 +225,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -440,7 +451,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
// Create keyrings directory if it doesn't exist
|
// Create keyrings directory if it doesn't exist
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
if err := mkdirCmd.Run(); err != nil {
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
}
|
}
|
||||||
@@ -454,13 +465,13 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -470,7 +481,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
@@ -490,7 +501,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
}
|
}
|
||||||
@@ -536,7 +547,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +635,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +653,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -681,7 +692,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -254,7 +255,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logError("failed to install prerequisites", err)
|
f.logError("failed to install prerequisites", err)
|
||||||
@@ -437,7 +438,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -461,7 +462,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -537,7 +538,7 @@ func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []st
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GentooGlobalUseFlags = []string{
|
var GentooGlobalUseFlags = []string{
|
||||||
@@ -201,9 +202,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if hasUse {
|
if hasUse {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
} else {
|
} else {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
@@ -281,7 +282,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
if syncErr != nil {
|
if syncErr != nil {
|
||||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
@@ -302,7 +303,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logError("failed to install prerequisites", err)
|
g.logError("failed to install prerequisites", err)
|
||||||
@@ -503,14 +504,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||||
packageUseDir := "/etc/portage/package.use"
|
packageUseDir := "/etc/portage/package.use"
|
||||||
|
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -524,7 +525,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -532,7 +533,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -557,7 +558,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable GURU repository
|
// Enable GURU repository
|
||||||
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
output, err := enableCmd.CombinedOutput()
|
output, err := enableCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -589,7 +590,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
LogOutput: "Syncing GURU repository",
|
LogOutput: "Syncing GURU repository",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -622,7 +623,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
|
|
||||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||||
|
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -636,7 +637,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -644,7 +645,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -695,6 +696,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualPackageInstaller provides methods for installing packages from source
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
@@ -143,7 +144,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
m.logError("failed to install dgop", err)
|
m.logError("failed to install dgop", err)
|
||||||
@@ -213,7 +214,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "dpkg -i niri.deb",
|
CommandInfo: "dpkg -i niri.deb",
|
||||||
}
|
}
|
||||||
|
|
||||||
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||||
|
|
||||||
output, err := installDebCmd.CombinedOutput()
|
output, err := installDebCmd.CombinedOutput()
|
||||||
@@ -324,7 +325,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -387,7 +388,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
@@ -453,7 +454,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
@@ -492,16 +493,11 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make it executable
|
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,15 +642,11 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -249,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
|
|||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
args := []string{"zypper", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logError("failed to install prerequisites", err)
|
o.logError("failed to install prerequisites", err)
|
||||||
@@ -485,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||||
@@ -506,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to refresh repositories: %w", err)
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
}
|
}
|
||||||
@@ -587,7 +588,7 @@ func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sud
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, alias := range aliases {
|
for _, alias := range aliases {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
|
||||||
repoOutput, err := cmd.CombinedOutput()
|
repoOutput, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
@@ -645,7 +646,7 @@ func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packag
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -797,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
|||||||
CommandInfo: "sudo zypper install rustup",
|
CommandInfo: "sudo zypper install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -177,7 +178,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -195,7 +196,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
// Not installed, install it
|
// Not installed, install it
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -211,7 +212,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -398,7 +399,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y software-properties-common")
|
"apt-get install -y software-properties-common")
|
||||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||||
@@ -416,7 +417,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||||
@@ -437,7 +438,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||||
}
|
}
|
||||||
@@ -504,7 +505,7 @@ func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []st
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +592,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,7 +610,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -649,7 +650,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||||
}
|
}
|
||||||
|
|
||||||
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||||
@@ -664,7 +665,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||||
}
|
}
|
||||||
@@ -678,7 +679,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package geolocation
|
|
||||||
|
|
||||||
type Location struct {
|
|
||||||
Latitude float64
|
|
||||||
Longitude float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
GetLocation() (Location, error)
|
|
||||||
|
|
||||||
Subscribe(id string) chan Location
|
|
||||||
Unsubscribe(id string)
|
|
||||||
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
+315
-200
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -14,6 +15,8 @@ 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/privesc"
|
||||||
"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"
|
||||||
@@ -343,56 +346,17 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"pacman -S --needed --noconfirm greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"dnf install -y greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||||
"zypper install -y greetd")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyUbuntu:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"apt-get install -y greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyDebian:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"apt-get install -y greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"emerge --ask n sys-apps/greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyNix:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
||||||
}
|
}
|
||||||
@@ -453,56 +417,56 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
|||||||
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
||||||
if _, err := exec.LookPath("gpg"); err != nil {
|
if _, err := exec.LookPath("gpg"); err != nil {
|
||||||
logFunc("Installing gnupg for OBS repository key import...")
|
logFunc("Installing gnupg for OBS repository key import...")
|
||||||
installGPGCmd := exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "gnupg")
|
installGPGCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y gnupg")
|
||||||
installGPGCmd.Stdout = os.Stdout
|
installGPGCmd.Stdout = os.Stdout
|
||||||
installGPGCmd.Stderr = os.Stderr
|
installGPGCmd.Stderr = os.Stderr
|
||||||
if err := installGPGCmd.Run(); err != nil {
|
if err := installGPGCmd.Run(); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mkdirCmd := exec.CommandContext(ctx, "sudo", "mkdir", "-p", "/etc/apt/keyrings")
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
mkdirCmd.Stdout = os.Stdout
|
mkdirCmd.Stdout = os.Stdout
|
||||||
mkdirCmd.Stderr = os.Stderr
|
mkdirCmd.Stderr = os.Stderr
|
||||||
mkdirCmd.Run()
|
mkdirCmd.Run()
|
||||||
addKeyCmd := exec.CommandContext(ctx, "bash", "-c",
|
addKeyCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL))
|
fmt.Sprintf(`bash -c "curl -fsSL %s | gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg"`, keyURL))
|
||||||
addKeyCmd.Stdout = os.Stdout
|
addKeyCmd.Stdout = os.Stdout
|
||||||
addKeyCmd.Stderr = os.Stderr
|
addKeyCmd.Stderr = os.Stderr
|
||||||
addKeyCmd.Run()
|
addKeyCmd.Run()
|
||||||
addRepoCmd := exec.CommandContext(ctx, "bash", "-c",
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine))
|
fmt.Sprintf(`bash -c "echo '%s' > /etc/apt/sources.list.d/danklinux.list"`, repoLine))
|
||||||
addRepoCmd.Stdout = os.Stdout
|
addRepoCmd.Stdout = os.Stdout
|
||||||
addRepoCmd.Stderr = os.Stderr
|
addRepoCmd.Stderr = os.Stderr
|
||||||
addRepoCmd.Run()
|
addRepoCmd.Run()
|
||||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
||||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
||||||
logFunc("Adding DankLinux OBS repository...")
|
logFunc("Adding DankLinux OBS repository...")
|
||||||
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL)
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo %s", repoURL))
|
||||||
addRepoCmd.Stdout = os.Stdout
|
addRepoCmd.Stdout = os.Stdout
|
||||||
addRepoCmd.Stderr = os.Stderr
|
addRepoCmd.Stderr = os.Stderr
|
||||||
addRepoCmd.Run()
|
addRepoCmd.Run()
|
||||||
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run()
|
privesc.ExecCommand(ctx, sudoPassword, "zypper refresh").Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y dms-greeter")
|
||||||
case distros.FamilyUbuntu:
|
case distros.FamilyUbuntu:
|
||||||
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
||||||
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
||||||
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux")
|
ppacmd := privesc.ExecCommand(ctx, sudoPassword, "add-apt-repository -y ppa:avengemedia/danklinux")
|
||||||
ppacmd.Stdout = os.Stdout
|
ppacmd.Stdout = os.Stdout
|
||||||
ppacmd.Stderr = os.Stderr
|
ppacmd.Stderr = os.Stderr
|
||||||
ppacmd.Run()
|
ppacmd.Run()
|
||||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
||||||
logFunc("Enabling COPR avengemedia/danklinux...")
|
logFunc("Enabling COPR avengemedia/danklinux...")
|
||||||
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux")
|
coprcmd := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y avengemedia/danklinux")
|
||||||
coprcmd.Stdout = os.Stdout
|
coprcmd.Stdout = os.Stdout
|
||||||
coprcmd.Stderr = os.Stderr
|
coprcmd.Stderr = os.Stderr
|
||||||
coprcmd.Run()
|
coprcmd.Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y dms-greeter")
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
aurHelper := ""
|
aurHelper := ""
|
||||||
for _, helper := range []string{"paru", "yay"} {
|
for _, helper := range []string{"paru", "yay"} {
|
||||||
@@ -555,25 +519,25 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
||||||
action = "Updated"
|
action = "Updated"
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
osInfo, err := distros.GetOSInfo()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
||||||
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logFunc("✓ Restored SELinux context for dms-greeter")
|
logFunc("✓ Restored SELinux context for dms-greeter")
|
||||||
@@ -599,7 +563,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to stat cache directory: %w", err)
|
return fmt.Errorf("failed to stat cache directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
created = true
|
created = true
|
||||||
@@ -611,17 +575,17 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
daemonUser := DetectGreeterUser()
|
daemonUser := DetectGreeterUser()
|
||||||
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
||||||
owner := preferredOwner
|
owner := preferredOwner
|
||||||
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||||
// Some setups may not have a matching daemon user at this moment; fall back
|
// Some setups may not have a matching daemon user at this moment; fall back
|
||||||
// to root:<group> while still allowing group-writable greeter runtime access.
|
// to root:<group> while still allowing group-writable greeter runtime access.
|
||||||
fallbackOwner := fmt.Sprintf("root:%s", group)
|
fallbackOwner := fmt.Sprintf("root:%s", group)
|
||||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||||
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
||||||
}
|
}
|
||||||
owner = fallbackOwner
|
owner = fallbackOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,13 +597,13 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
filepath.Join(cacheDir, ".cache"),
|
filepath.Join(cacheDir, ".cache"),
|
||||||
}
|
}
|
||||||
for _, dir := range runtimeDirs {
|
for _, dir := range runtimeDirs {
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, dir); err != nil {
|
||||||
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", dir); err != nil {
|
||||||
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,7 +615,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
||||||
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,13 +640,13 @@ func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPat
|
|||||||
info, err := os.Lstat(legacyPath)
|
info, err := os.Lstat(legacyPath)
|
||||||
if err == nil && info.Mode().IsRegular() {
|
if err == nil && info.Mode().IsRegular() {
|
||||||
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
||||||
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
if copyErr := privesc.Run(context.Background(), sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||||
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +673,7 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,15 +690,15 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.CommandExists("apparmor_parser") {
|
if utils.CommandExists("apparmor_parser") {
|
||||||
if err := runSudoCmd(sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
||||||
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
||||||
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
||||||
} else {
|
} else {
|
||||||
@@ -781,10 +745,10 @@ func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) err
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to write PAM config: %w", err)
|
return fmt.Errorf("failed to write PAM config: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to set PAM config permissions: %w", err)
|
return fmt.Errorf("failed to set PAM config permissions: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
|
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
|
||||||
@@ -805,9 +769,9 @@ func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if utils.CommandExists("apparmor_parser") {
|
if utils.CommandExists("apparmor_parser") {
|
||||||
_ = runSudoCmd(sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
_ = privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Removed DMS AppArmor profile")
|
logFunc(" ✓ Removed DMS AppArmor profile")
|
||||||
@@ -837,50 +801,17 @@ func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl")
|
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||||
} else {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyUbuntu:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyDebian:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyNix:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
||||||
}
|
}
|
||||||
@@ -937,7 +868,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
||||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
||||||
continue
|
continue
|
||||||
@@ -994,7 +925,7 @@ func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, user := range existingUsers {
|
for _, user := range existingUsers {
|
||||||
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
_ = privesc.Run(context.Background(), sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||||
cleaned = true
|
cleaned = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1034,7 +965,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
||||||
if !utils.HasGroup(group) {
|
if !utils.HasGroup(group) {
|
||||||
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "groupadd", "-r", group); err != nil {
|
||||||
return fmt.Errorf("failed to create %s group: %w", group, err)
|
return fmt.Errorf("failed to create %s group: %w", group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
||||||
@@ -1045,7 +976,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||||
} else {
|
} else {
|
||||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||||
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||||
@@ -1060,7 +991,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
if strings.Contains(string(daemonGroupsOutput), group) {
|
if strings.Contains(string(daemonGroupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
||||||
} else {
|
} else {
|
||||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
||||||
} else {
|
} else {
|
||||||
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
||||||
@@ -1075,6 +1006,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
}{
|
}{
|
||||||
{filepath.Join(homeDir, ".config", "DankMaterialShell"), "DankMaterialShell config"},
|
{filepath.Join(homeDir, ".config", "DankMaterialShell"), "DankMaterialShell config"},
|
||||||
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
|
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
|
||||||
|
{filepath.Join(homeDir, ".cache", "DankMaterialShell"), "DankMaterialShell cache"},
|
||||||
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
|
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
|
||||||
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
||||||
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
|
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
|
||||||
@@ -1089,12 +1021,12 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1109,6 +1041,217 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GreeterColorSyncInfo struct {
|
||||||
|
SourcePath string
|
||||||
|
ThemeName string
|
||||||
|
UsesDynamicWallpaperOverride bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type greeterThemeSyncSettings struct {
|
||||||
|
CurrentThemeName string `json:"currentThemeName"`
|
||||||
|
GreeterWallpaperPath string `json:"greeterWallpaperPath"`
|
||||||
|
MatugenScheme string `json:"matugenScheme"`
|
||||||
|
IconTheme string `json:"iconTheme"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type greeterThemeSyncSession struct {
|
||||||
|
IsLightMode bool `json:"isLightMode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type greeterThemeSyncState struct {
|
||||||
|
ThemeName string
|
||||||
|
GreeterWallpaperPath string
|
||||||
|
ResolvedGreeterWallpaperPath string
|
||||||
|
MatugenScheme string
|
||||||
|
IconTheme string
|
||||||
|
IsLightMode bool
|
||||||
|
UsesDynamicWallpaperOverride bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultGreeterColorsSource(homeDir string) string {
|
||||||
|
return filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func greeterOverrideColorsStateDir(homeDir string) string {
|
||||||
|
return filepath.Join(homeDir, ".cache", "DankMaterialShell", "greeter-colors")
|
||||||
|
}
|
||||||
|
|
||||||
|
func greeterOverrideColorsSource(homeDir string) string {
|
||||||
|
return filepath.Join(greeterOverrideColorsStateDir(homeDir), "dms-colors.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOptionalJSONFile(path string, dst any) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(data)) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGreeterThemeSyncSettings(homeDir string) (greeterThemeSyncSettings, error) {
|
||||||
|
settings := greeterThemeSyncSettings{
|
||||||
|
CurrentThemeName: "purple",
|
||||||
|
MatugenScheme: "scheme-tonal-spot",
|
||||||
|
IconTheme: "System Default",
|
||||||
|
}
|
||||||
|
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||||
|
if err := readOptionalJSONFile(settingsPath, &settings); err != nil {
|
||||||
|
return greeterThemeSyncSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGreeterThemeSyncSession(homeDir string) (greeterThemeSyncSession, error) {
|
||||||
|
session := greeterThemeSyncSession{}
|
||||||
|
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
|
||||||
|
if err := readOptionalJSONFile(sessionPath, &session); err != nil {
|
||||||
|
return greeterThemeSyncSession{}, fmt.Errorf("failed to parse session at %s: %w", sessionPath, err)
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveGreeterThemeSyncState(homeDir string) (greeterThemeSyncState, error) {
|
||||||
|
settings, err := readGreeterThemeSyncSettings(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return greeterThemeSyncState{}, err
|
||||||
|
}
|
||||||
|
session, err := readGreeterThemeSyncSession(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return greeterThemeSyncState{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedWallpaperPath := ""
|
||||||
|
if settings.GreeterWallpaperPath != "" {
|
||||||
|
resolvedWallpaperPath = settings.GreeterWallpaperPath
|
||||||
|
if !filepath.IsAbs(resolvedWallpaperPath) {
|
||||||
|
resolvedWallpaperPath = filepath.Join(homeDir, resolvedWallpaperPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usesDynamicWallpaperOverride := strings.EqualFold(strings.TrimSpace(settings.CurrentThemeName), "dynamic") && resolvedWallpaperPath != ""
|
||||||
|
|
||||||
|
return greeterThemeSyncState{
|
||||||
|
ThemeName: settings.CurrentThemeName,
|
||||||
|
GreeterWallpaperPath: settings.GreeterWallpaperPath,
|
||||||
|
ResolvedGreeterWallpaperPath: resolvedWallpaperPath,
|
||||||
|
MatugenScheme: settings.MatugenScheme,
|
||||||
|
IconTheme: settings.IconTheme,
|
||||||
|
IsLightMode: session.IsLightMode,
|
||||||
|
UsesDynamicWallpaperOverride: usesDynamicWallpaperOverride,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s greeterThemeSyncState) effectiveColorsSource(homeDir string) string {
|
||||||
|
if s.UsesDynamicWallpaperOverride {
|
||||||
|
return greeterOverrideColorsSource(homeDir)
|
||||||
|
}
|
||||||
|
return defaultGreeterColorsSource(homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveGreeterColorSyncInfo(homeDir string) (GreeterColorSyncInfo, error) {
|
||||||
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return GreeterColorSyncInfo{}, err
|
||||||
|
}
|
||||||
|
return GreeterColorSyncInfo{
|
||||||
|
SourcePath: state.effectiveColorsSource(homeDir),
|
||||||
|
ThemeName: state.ThemeName,
|
||||||
|
UsesDynamicWallpaperOverride: state.UsesDynamicWallpaperOverride,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureGreeterSyncSourceFile(path string) error {
|
||||||
|
sourceDir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create source directory %s: %w", sourceDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(path, []byte("{}"), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to create source file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to inspect source file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGreeterDynamicOverrideColors(dmsPath, homeDir string, state greeterThemeSyncState, logFunc func(string)) error {
|
||||||
|
if !state.UsesDynamicWallpaperOverride {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := os.Stat(state.ResolvedGreeterWallpaperPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", state.ResolvedGreeterWallpaperPath, err)
|
||||||
|
}
|
||||||
|
if st.IsDir() {
|
||||||
|
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", state.ResolvedGreeterWallpaperPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := matugen.ColorModeDark
|
||||||
|
if state.IsLightMode {
|
||||||
|
mode = matugen.ColorModeLight
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := matugen.Options{
|
||||||
|
StateDir: greeterOverrideColorsStateDir(homeDir),
|
||||||
|
ShellDir: dmsPath,
|
||||||
|
ConfigDir: filepath.Join(homeDir, ".config"),
|
||||||
|
Kind: "image",
|
||||||
|
Value: state.ResolvedGreeterWallpaperPath,
|
||||||
|
Mode: mode,
|
||||||
|
IconTheme: state.IconTheme,
|
||||||
|
MatugenType: state.MatugenScheme,
|
||||||
|
RunUserTemplates: false,
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
logFunc("✓ Greeter dynamic override colors already up to date")
|
||||||
|
return nil
|
||||||
|
case err != nil:
|
||||||
|
return fmt.Errorf("failed to generate greeter dynamic colors from wallpaper override: %w", err)
|
||||||
|
default:
|
||||||
|
logFunc("✓ Generated greeter dynamic colors from wallpaper override")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncState, logFunc func(string), sudoPassword string) error {
|
||||||
|
source := state.effectiveColorsSource(homeDir)
|
||||||
|
if !state.UsesDynamicWallpaperOverride {
|
||||||
|
if err := ensureGreeterSyncSourceFile(source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if _, err := os.Stat(source); err != nil {
|
||||||
|
return fmt.Errorf("expected generated greeter colors at %s: %w", source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := filepath.Join(cacheDir, "colors.json")
|
||||||
|
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", target)
|
||||||
|
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", source, target); err != nil {
|
||||||
|
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.UsesDynamicWallpaperOverride {
|
||||||
|
logFunc("✓ Synced wallpaper based theming (greeter wallpaper override)")
|
||||||
|
} else {
|
||||||
|
logFunc("✓ Synced wallpaper based theming")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, forceAuth bool) error {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1132,11 +1275,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
target: filepath.Join(cacheDir, "session.json"),
|
target: filepath.Join(cacheDir, "session.json"),
|
||||||
desc: "state (wallpaper configuration)",
|
desc: "state (wallpaper configuration)",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
target: filepath.Join(cacheDir, "colors.json"),
|
|
||||||
desc: "wallpaper based theming",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, link := range symlinks {
|
for _, link := range symlinks {
|
||||||
@@ -1153,16 +1291,29 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", link.target)
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||||
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syncGreeterWallpaperOverride(homeDir, cacheDir, logFunc, sudoPassword); err != nil {
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve greeter color source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syncGreeterDynamicOverrideColors(dmsPath, homeDir, state, logFunc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syncGreeterColorSource(homeDir, cacheDir, state, logFunc, sudoPassword); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syncGreeterWallpaperOverride(cacheDir, logFunc, sudoPassword, state); err != nil {
|
||||||
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,36 +1332,19 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string), sudoPassword string) error {
|
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
||||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
|
||||||
data, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
var settings struct {
|
|
||||||
GreeterWallpaperPath string `json:"greeterWallpaperPath"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &settings); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
if settings.GreeterWallpaperPath == "" {
|
if state.ResolvedGreeterWallpaperPath == "" {
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Cleared greeter wallpaper override")
|
logFunc("✓ Cleared greeter wallpaper override")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
src := settings.GreeterWallpaperPath
|
src := state.ResolvedGreeterWallpaperPath
|
||||||
if !filepath.IsAbs(src) {
|
|
||||||
src = filepath.Join(homeDir, src)
|
|
||||||
}
|
|
||||||
st, err := os.Stat(src)
|
st, err := os.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err)
|
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err)
|
||||||
@@ -1218,17 +1352,17 @@ func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string)
|
|||||||
if st.IsDir() {
|
if st.IsDir() {
|
||||||
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", src, destPath); err != nil {
|
||||||
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
daemonUser := DetectGreeterUser()
|
daemonUser := DetectGreeterUser()
|
||||||
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Synced greeter wallpaper override")
|
logFunc("✓ Synced greeter wallpaper override")
|
||||||
@@ -1593,10 +1727,10 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str
|
|||||||
if err := tmpFile.Close(); err != nil {
|
if err := tmpFile.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
|
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
||||||
}
|
}
|
||||||
if wantFprint || wantU2f {
|
if wantFprint || wantU2f {
|
||||||
@@ -1655,13 +1789,13 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
greeterDir := "/etc/greetd/niri"
|
greeterDir := "/etc/greetd/niri"
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1683,7 +1817,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1706,7 +1840,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1782,7 +1916,7 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
|
|||||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
||||||
return fmt.Errorf("failed to update greetd config: %w", err)
|
return fmt.Errorf("failed to update greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1798,10 +1932,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
||||||
if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", path, backupPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return runSudoCmd(sudoPassword, "chmod", "644", backupPath)
|
return privesc.Run(context.Background(), sudoPassword, "chmod", "644", backupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *niriGreeterSync) processFile(filePath string) error {
|
func (s *niriGreeterSync) processFile(filePath string) error {
|
||||||
@@ -2014,6 +2148,8 @@ 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 != "" {
|
||||||
@@ -2037,11 +2173,11 @@ vt = 1
|
|||||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd config: %w", err)
|
return fmt.Errorf("failed to install greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2145,27 +2281,6 @@ func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
|
|||||||
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
|
|
||||||
if sudoPassword != "" {
|
|
||||||
fullArgs := append([]string{command}, args...)
|
|
||||||
quotedArgs := make([]string, len(fullArgs))
|
|
||||||
for i, arg := range fullArgs {
|
|
||||||
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
|
||||||
}
|
|
||||||
cmdStr := strings.Join(quotedArgs, " ")
|
|
||||||
|
|
||||||
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSystemdEnabled(service string) (string, error) {
|
func checkSystemdEnabled(service string) (string, error) {
|
||||||
cmd := exec.Command("systemctl", "is-enabled", service)
|
cmd := exec.Command("systemctl", "is-enabled", service)
|
||||||
output, _ := cmd.Output()
|
output, _ := cmd.Output()
|
||||||
@@ -2182,7 +2297,7 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
|||||||
switch state {
|
switch state {
|
||||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||||
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "disable", dm); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "disable", dm); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
||||||
} else {
|
} else {
|
||||||
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
||||||
@@ -2203,13 +2318,13 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
|||||||
}
|
}
|
||||||
if state == "masked" || state == "masked-runtime" {
|
if state == "masked" || state == "masked-runtime" {
|
||||||
logFunc(" Unmasking greetd...")
|
logFunc(" Unmasking greetd...")
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Unmasked greetd")
|
logFunc(" ✓ Unmasked greetd")
|
||||||
}
|
}
|
||||||
logFunc(" Enabling greetd service (--force)...")
|
logFunc(" Enabling greetd service (--force)...")
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
}
|
}
|
||||||
logFunc("✓ greetd enabled")
|
logFunc("✓ greetd enabled")
|
||||||
@@ -2229,7 +2344,7 @@ func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
||||||
return fmt.Errorf("failed to set graphical target: %w", err)
|
return fmt.Errorf("failed to set graphical target: %w", err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Default target set to graphical.target")
|
logFunc("✓ Default target set to graphical.target")
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package greeter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTestJSON(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
settingsJSON string
|
||||||
|
sessionJSON string
|
||||||
|
wantSourcePath string
|
||||||
|
wantResolvedWallpaper string
|
||||||
|
wantDynamicOverrideUsed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": "Pictures/blue.jpg",
|
||||||
|
"matugenScheme": "scheme-tonal-spot",
|
||||||
|
"iconTheme": "Papirus"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":true}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
||||||
|
wantDynamicOverrideUsed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic theme without override uses desktop colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": ""
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "purple",
|
||||||
|
"greeterWallpaperPath": "/tmp/blue.jpg"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "/tmp/blue.jpg",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||||
|
|
||||||
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
||||||
|
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
||||||
|
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
||||||
|
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
||||||
|
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
||||||
|
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,7 +291,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 := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,103 @@ 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,
|
||||||
@@ -91,7 +188,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,7 +256,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 := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(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,9 +3,74 @@ 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
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
{ID: "zed", Commands: []string{"zed"}, ConfigFile: "zed.toml"},
|
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
@@ -100,6 +100,7 @@ type Options struct {
|
|||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -274,6 +275,10 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
if isDMSGTKActive(opts.ConfigDir) {
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case ColorModeLight:
|
case ColorModeLight:
|
||||||
@@ -331,6 +336,10 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -597,10 +606,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
|||||||
matugenVersionOK = true
|
matugenVersionOK = true
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenSupportsCOE {
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||||
}
|
}
|
||||||
if matugenIsV4 {
|
if matugenIsV4 {
|
||||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||||
}
|
}
|
||||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write base config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: filepath.Join(tempDir, "config"),
|
||||||
|
StateDir: filepath.Join(tempDir, "state"),
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(output)
|
||||||
|
assert.Contains(t, content, "[templates.dank]")
|
||||||
|
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||||
|
assert.NotContains(t, content, "[templates.gtk]")
|
||||||
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
// 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,62 +1062,6 @@ 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()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -59,7 +60,11 @@ func Send(n Notification) error {
|
|||||||
|
|
||||||
hints := map[string]dbus.Variant{}
|
hints := map[string]dbus.Variant{}
|
||||||
if n.FilePath != "" {
|
if n.FilePath != "" {
|
||||||
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
imgPath := n.FilePath
|
||||||
|
if !strings.HasPrefix(imgPath, "file://") {
|
||||||
|
imgPath = "file://" + imgPath
|
||||||
|
}
|
||||||
|
hints["image_path"] = dbus.MakeVariant(imgPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := conn.Object(notifyDest, notifyPath)
|
obj := conn.Object(notifyDest, notifyPath)
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
package privesc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool identifies a privilege-escalation binary.
|
||||||
|
type Tool string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ToolSudo Tool = "sudo"
|
||||||
|
ToolDoas Tool = "doas"
|
||||||
|
ToolRun0 Tool = "run0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvVar selects a specific tool when set to one of: sudo, doas, run0.
|
||||||
|
const EnvVar = "DMS_PRIVESC"
|
||||||
|
|
||||||
|
var detectionOrder = []Tool{ToolSudo, ToolDoas, ToolRun0}
|
||||||
|
|
||||||
|
var (
|
||||||
|
detectOnce sync.Once
|
||||||
|
detected Tool
|
||||||
|
detectErr error
|
||||||
|
userSelected bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Detect returns the tool that should be used for privilege escalation.
|
||||||
|
// The result is cached after the first call.
|
||||||
|
func Detect() (Tool, error) {
|
||||||
|
detectOnce.Do(func() {
|
||||||
|
detected, detectErr = detectTool()
|
||||||
|
})
|
||||||
|
return detected, detectErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetForTesting clears cached detection state.
|
||||||
|
func ResetForTesting() {
|
||||||
|
detectOnce = sync.Once{}
|
||||||
|
detected = ""
|
||||||
|
detectErr = nil
|
||||||
|
userSelected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvailableTools returns the set of supported tools that are installed on
|
||||||
|
// PATH, in detection-precedence order.
|
||||||
|
func AvailableTools() []Tool {
|
||||||
|
var out []Tool
|
||||||
|
for _, t := range detectionOrder {
|
||||||
|
if t.Available() {
|
||||||
|
out = append(out, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvOverride returns the tool selected by the $DMS_PRIVESC env var (if any)
|
||||||
|
// along with ok=true when the variable is set. An empty or unset variable
|
||||||
|
// returns ok=false.
|
||||||
|
func EnvOverride() (Tool, bool) {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar)))
|
||||||
|
if v == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return Tool(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTool forces the detected tool to t, bypassing autodetection. Intended
|
||||||
|
// for use after the caller has prompted the user for a selection.
|
||||||
|
func SetTool(t Tool) error {
|
||||||
|
if !t.Available() {
|
||||||
|
return fmt.Errorf("%q is not installed", t.Name())
|
||||||
|
}
|
||||||
|
detectOnce = sync.Once{}
|
||||||
|
detectOnce.Do(func() {
|
||||||
|
detected = t
|
||||||
|
detectErr = nil
|
||||||
|
})
|
||||||
|
userSelected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectTool() (Tool, error) {
|
||||||
|
switch override := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar))); override {
|
||||||
|
case "":
|
||||||
|
// fall through to autodetect
|
||||||
|
case string(ToolSudo), string(ToolDoas), string(ToolRun0):
|
||||||
|
t := Tool(override)
|
||||||
|
if !t.Available() {
|
||||||
|
return "", fmt.Errorf("%s=%s but %q is not installed", EnvVar, override, t.Name())
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid %s=%q: must be one of sudo, doas, run0", EnvVar, override)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range detectionOrder {
|
||||||
|
if t.Available() {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no supported privilege escalation tool found (tried: sudo, doas, run0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the binary name.
|
||||||
|
func (t Tool) Name() string { return string(t) }
|
||||||
|
|
||||||
|
// Available reports whether this tool's binary is on PATH.
|
||||||
|
func (t Tool) Available() bool {
|
||||||
|
if t == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := exec.LookPath(string(t))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsStdinPassword reports whether the tool can accept a password via
|
||||||
|
// stdin. Only sudo (-S) supports this.
|
||||||
|
func (t Tool) SupportsStdinPassword() bool {
|
||||||
|
return t == ToolSudo
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeSingleQuotes escapes single quotes for safe inclusion inside a
|
||||||
|
// bash single-quoted string.
|
||||||
|
func EscapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeCommand returns a bash command string that runs `command` with the
|
||||||
|
// detected tool. When the tool supports stdin passwords and password is
|
||||||
|
// non-empty, the password is piped in. Otherwise the tool is invoked with
|
||||||
|
// no non-interactive flag so that an interactive TTY prompt is still
|
||||||
|
// possible for CLI callers.
|
||||||
|
//
|
||||||
|
// If detection fails, the returned shell string exits 1 with an error
|
||||||
|
// message so callers that treat the *exec.Cmd as infallible still fail
|
||||||
|
// deterministically.
|
||||||
|
func MakeCommand(password, command string) string {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return failingShell(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
if password != "" {
|
||||||
|
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("sudo %s", command)
|
||||||
|
case ToolDoas:
|
||||||
|
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
|
||||||
|
case ToolRun0:
|
||||||
|
return fmt.Sprintf("run0 sh -c '%s'", EscapeSingleQuotes(command))
|
||||||
|
default:
|
||||||
|
return failingShell(fmt.Errorf("unsupported privilege tool: %q", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCommand builds an exec.Cmd that runs `command` as root via the
|
||||||
|
// detected tool. Detection errors surface at Run() time as a failing
|
||||||
|
// command writing a clear error to stderr.
|
||||||
|
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
|
||||||
|
// No stdin password is supplied; callers relying on non-interactive success
|
||||||
|
// should ensure cached credentials are present (see CheckCached).
|
||||||
|
func ExecArgv(ctx context.Context, argv ...string) *exec.Cmd {
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("privesc.ExecArgv: argv must not be empty")))
|
||||||
|
}
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case ToolSudo, ToolDoas:
|
||||||
|
return exec.CommandContext(ctx, string(t), argv...)
|
||||||
|
case ToolRun0:
|
||||||
|
return exec.CommandContext(ctx, "run0", argv...)
|
||||||
|
default:
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("unsupported privilege tool: %q", t)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func failingShell(err error) string {
|
||||||
|
return fmt.Sprintf("printf 'privesc: %%s\\n' '%s' >&2; exit 1", EscapeSingleQuotes(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckCached runs a non-interactive credential probe. Returns nil if the
|
||||||
|
// tool will run commands without prompting (cached credentials, nopass, or
|
||||||
|
// polkit rule).
|
||||||
|
func CheckCached(ctx context.Context) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
cmd = exec.CommandContext(ctx, "sudo", "-n", "true")
|
||||||
|
case ToolDoas:
|
||||||
|
cmd = exec.CommandContext(ctx, "doas", "-n", "true")
|
||||||
|
case ToolRun0:
|
||||||
|
cmd = exec.CommandContext(ctx, "run0", "--no-ask-password", "true")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported privilege tool: %q", t)
|
||||||
|
}
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCache invalidates any cached credentials. No-op for tools that do
|
||||||
|
// not expose a cache-clear operation.
|
||||||
|
func ClearCache(ctx context.Context) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
return exec.CommandContext(ctx, "sudo", "-k").Run()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWithAskpass validates cached credentials using an askpass helper
|
||||||
|
// script. Only sudo supports this mechanism; the TUI uses it to trigger
|
||||||
|
// fingerprint authentication via PAM.
|
||||||
|
func ValidateWithAskpass(ctx context.Context, askpassScript string) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != ToolSudo {
|
||||||
|
return fmt.Errorf("askpass validation requires sudo (detected: %s)", t)
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePassword validates the given password. Only sudo supports this
|
||||||
|
// (via `sudo -S -v`); for other tools the caller should fall back to
|
||||||
|
// CheckCached.
|
||||||
|
func ValidatePassword(ctx context.Context, password string) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != ToolSudo {
|
||||||
|
return fmt.Errorf("password validation requires sudo (detected: %s)", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(stdin, "%s\n", password); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
_ = cmd.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuoteArgsForShell wraps each argv element in single quotes so the result
|
||||||
|
// can be safely passed to bash -c.
|
||||||
|
func QuoteArgsForShell(argv []string) string {
|
||||||
|
parts := make([]string, len(argv))
|
||||||
|
for i, a := range argv {
|
||||||
|
parts[i] = "'" + EscapeSingleQuotes(a) + "'"
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run invokes argv with privilege escalation. When the tool supports stdin
|
||||||
|
// passwords and password is non-empty, the password is piped in. Otherwise
|
||||||
|
// argv is invoked directly, which may prompt on a TTY.
|
||||||
|
// Stdout and Stderr are inherited from the current process.
|
||||||
|
func Run(ctx context.Context, password string, argv ...string) error {
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return fmt.Errorf("privesc.Run: argv must not be empty")
|
||||||
|
}
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch {
|
||||||
|
case t == ToolSudo && password != "":
|
||||||
|
cmd = ExecCommand(ctx, password, QuoteArgsForShell(argv))
|
||||||
|
default:
|
||||||
|
cmd = ExecArgv(ctx, argv...)
|
||||||
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdinIsTTY reports whether stdin is a character device (interactive
|
||||||
|
// terminal) rather than a pipe or file.
|
||||||
|
func stdinIsTTY() bool {
|
||||||
|
fi, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptCLI interactively prompts the user to pick a privilege tool when more
|
||||||
|
// than one is installed and $DMS_PRIVESC is not set. If stdin is not a TTY,
|
||||||
|
// or only one tool is available, or the env var is set, the detected tool is
|
||||||
|
// returned without any prompt.
|
||||||
|
//
|
||||||
|
// The prompt is written to out (typically os.Stdout/os.Stderr) and input is
|
||||||
|
// read from in. EOF or empty input selects the first option.
|
||||||
|
func PromptCLI(out io.Writer, in io.Reader) (Tool, error) {
|
||||||
|
if userSelected {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
if _, envSet := EnvOverride(); envSet {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
|
||||||
|
tools := AvailableTools()
|
||||||
|
switch len(tools) {
|
||||||
|
case 0:
|
||||||
|
return "", fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||||
|
case 1:
|
||||||
|
if err := SetTool(tools[0]); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tools[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stdinIsTTY() {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(out, "Multiple privilege escalation tools detected:")
|
||||||
|
for i, t := range tools {
|
||||||
|
fmt.Fprintf(out, " [%d] %s\n", i+1, t.Name())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, "Choose one [1-%d] (default 1, or set %s=<tool> to skip): ", len(tools), EnvVar)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(in)
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return "", fmt.Errorf("failed to read selection: %w", err)
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
idx := 1
|
||||||
|
if line != "" {
|
||||||
|
n, convErr := strconv.Atoi(line)
|
||||||
|
if convErr != nil || n < 1 || n > len(tools) {
|
||||||
|
return "", fmt.Errorf("invalid selection %q", line)
|
||||||
|
}
|
||||||
|
idx = n
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen := tools[idx-1]
|
||||||
|
if err := SetTool(chosen); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return chosen, nil
|
||||||
|
}
|
||||||
@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
|
|||||||
|
|
||||||
type outputInfo struct {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
func getAllOutputInfos() map[string]*outputInfo {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
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, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
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
|
||||||
})
|
})
|
||||||
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*outputInfo, len(heads))
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == outputName {
|
if state.name == "" {
|
||||||
return &outputInfo{
|
continue
|
||||||
x: state.x,
|
}
|
||||||
y: state.y,
|
result[state.name] = &outputInfo{
|
||||||
transform: state.transform,
|
x: state.x,
|
||||||
}, true
|
y: state.y,
|
||||||
|
scale: state.scale,
|
||||||
|
transform: state.transform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
return nil, false
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
|
infos := getAllOutputInfos()
|
||||||
|
if infos == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
info, ok := infos[outputName]
|
||||||
|
return info, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -298,22 +299,20 @@ 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])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture all outputs first to get actual buffer sizes
|
wlrInfos := getAllOutputInfos()
|
||||||
type capturedOutput struct {
|
|
||||||
output *WaylandOutput
|
|
||||||
result *CaptureResult
|
|
||||||
physX int
|
|
||||||
physY int
|
|
||||||
}
|
|
||||||
captured := make([]capturedOutput, 0, len(outputs))
|
|
||||||
|
|
||||||
var minX, minY, maxX, maxY int
|
type pendingOutput struct {
|
||||||
first := true
|
result *CaptureResult
|
||||||
|
logX float64
|
||||||
|
logY float64
|
||||||
|
scale float64
|
||||||
|
}
|
||||||
|
var pending []pendingOutput
|
||||||
|
maxScale := 1.0
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -322,50 +321,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outX, outY := output.x, output.y
|
logX, logY := float64(output.x), float64(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 {
|
||||||
outX, outY = hx, hy
|
logX, logY = float64(hx), float64(hy)
|
||||||
}
|
}
|
||||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||||
scale = s
|
scale = hs
|
||||||
}
|
}
|
||||||
case CompositorDWL:
|
default:
|
||||||
if info, ok := getOutputInfo(output.name); ok {
|
if wlrInfos != nil {
|
||||||
outX, outY = info.x, info.y
|
if info, ok := wlrInfos[output.name]; ok {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
physX := int(float64(outX) * scale)
|
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||||
physY := int(float64(outY) * scale)
|
if scale > maxScale {
|
||||||
|
maxScale = scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
captured = append(captured, capturedOutput{
|
if len(pending) == 0 {
|
||||||
output: output,
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
result: result,
|
}
|
||||||
physX: physX,
|
if len(pending) == 1 {
|
||||||
physY: physY,
|
return pending[0].result, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
right := physX + result.Buffer.Width
|
type layoutEntry struct {
|
||||||
bottom := physY + result.Buffer.Height
|
result *CaptureResult
|
||||||
|
canvasX int
|
||||||
|
canvasY int
|
||||||
|
canvasW int
|
||||||
|
canvasH int
|
||||||
|
}
|
||||||
|
entries := make([]layoutEntry, len(pending))
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
|
||||||
if first {
|
for i, p := range pending {
|
||||||
minX, minY = physX, physY
|
cx := int(math.Round(p.logX * maxScale))
|
||||||
maxX, maxY = right, bottom
|
cy := int(math.Round(p.logY * maxScale))
|
||||||
first = false
|
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
||||||
|
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
||||||
|
|
||||||
|
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
||||||
|
|
||||||
|
right := cx + cw
|
||||||
|
bottom := cy + ch
|
||||||
|
if i == 0 {
|
||||||
|
minX, minY, maxX, maxY = cx, cy, right, bottom
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if cx < minX {
|
||||||
if physX < minX {
|
minX = cx
|
||||||
minX = physX
|
|
||||||
}
|
}
|
||||||
if physY < minY {
|
if cy < minY {
|
||||||
minY = physY
|
minY = cy
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
maxX = right
|
||||||
@@ -375,35 +398,26 @@ 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 _, c := range captured {
|
for _, e := range entries {
|
||||||
c.result.Buffer.Close()
|
e.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 _, c := range captured {
|
for _, e := range entries {
|
||||||
if format == 0 {
|
if format == 0 {
|
||||||
format = c.result.Format
|
format = e.result.Format
|
||||||
}
|
}
|
||||||
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
s.blitBufferScaled(composite, e.result.Buffer,
|
||||||
c.result.Buffer.Close()
|
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||||
|
e.result.YInverted)
|
||||||
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -413,32 +427,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
||||||
|
if dstW <= 0 || dstH <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
srcData := src.Data()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for srcY := 0; srcY < src.Height; srcY++ {
|
for dy := 0; dy < dstH; dy++ {
|
||||||
actualSrcY := srcY
|
canvasY := dstY + dy
|
||||||
if yInverted {
|
if canvasY < 0 || canvasY >= dst.Height {
|
||||||
actualSrcY = src.Height - 1 - srcY
|
|
||||||
}
|
|
||||||
|
|
||||||
dy := dstY + srcY
|
|
||||||
if dy < 0 || dy >= dst.Height {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := actualSrcY * src.Stride
|
srcY := dy * src.Height / dstH
|
||||||
dstRowOff := dy * dst.Stride
|
if yInverted {
|
||||||
|
srcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= src.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for srcX := 0; srcX < src.Width; srcX++ {
|
srcRowOff := srcY * src.Stride
|
||||||
dx := dstX + srcX
|
dstRowOff := canvasY * dst.Stride
|
||||||
if dx < 0 || dx >= dst.Width {
|
|
||||||
|
for dx := 0; dx < dstW; dx++ {
|
||||||
|
canvasX := dstX + dx
|
||||||
|
if canvasX < 0 || canvasX >= dst.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcX := dx * src.Width / dstW
|
||||||
|
if srcX >= src.Width {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + dx*4
|
di := dstRowOff + canvasX*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -215,31 +215,34 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
if timer, exists := b.debounceTimers[id]; exists {
|
if existing, exists := b.debounceTimers[id]; exists {
|
||||||
timer.Reset(200 * time.Millisecond)
|
if existing.Stop() {
|
||||||
} else {
|
b.debounceWg.Done()
|
||||||
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
|
||||||
@@ -490,5 +493,19 @@ 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,6 +84,7 @@ 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 {
|
||||||
|
|||||||
@@ -212,9 +212,10 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var offer any
|
var offer any
|
||||||
if e.Id != nil {
|
switch {
|
||||||
|
case e.Id != nil:
|
||||||
offer = e.Id
|
offer = e.Id
|
||||||
} else if e.OfferId != 0 {
|
case 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()
|
||||||
@@ -224,10 +225,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -236,9 +233,11 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
m.currentOffer = offer
|
m.currentOffer = offer
|
||||||
|
|
||||||
if prevOffer != nil && prevOffer != offer {
|
if prevOffer != nil && prevOffer != offer {
|
||||||
m.offerMutex.Lock()
|
m.releaseOffer(prevOffer)
|
||||||
delete(m.offerMimeTypes, prevOffer)
|
}
|
||||||
m.offerMutex.Unlock()
|
|
||||||
|
if offer == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
@@ -292,6 +291,33 @@ 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()
|
||||||
|
|
||||||
@@ -395,7 +421,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|||||||
if extractHash(v) != hash {
|
if extractHash(v) != hash {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -413,7 +439,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -456,6 +482,14 @@ 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
|
||||||
|
|
||||||
@@ -463,8 +497,15 @@ func decodeEntry(data []byte) (Entry, error) {
|
|||||||
|
|
||||||
var dataLen uint32
|
var dataLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||||
e.Data = make([]byte, dataLen)
|
switch {
|
||||||
buf.Read(e.Data)
|
case withData:
|
||||||
|
e.Data = make([]byte, dataLen)
|
||||||
|
buf.Read(e.Data)
|
||||||
|
default:
|
||||||
|
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mimeLen uint32
|
var mimeLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||||
@@ -668,14 +709,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,7 +786,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -935,7 +971,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
toDelete = append(toDelete, k)
|
toDelete = append(toDelete, k)
|
||||||
}
|
}
|
||||||
@@ -958,7 +994,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, _ := decodeEntry(v)
|
entry, _ := decodeEntryMeta(v)
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1102,7 @@ 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}
|
||||||
@@ -1145,9 +1182,11 @@ func (m *Manager) Close() {
|
|||||||
m.subscribers = make(map[string]chan State)
|
m.subscribers = make(map[string]chan State)
|
||||||
m.subMutex.Unlock()
|
m.subMutex.Unlock()
|
||||||
|
|
||||||
if m.currentSource != nil {
|
m.releaseCurrentSource()
|
||||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
source.Destroy()
|
if m.currentOffer != nil {
|
||||||
|
m.releaseOffer(m.currentOffer)
|
||||||
|
m.currentOffer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.dataDevice != nil {
|
if m.dataDevice != nil {
|
||||||
@@ -1191,11 +1230,10 @@ 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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Skip pinned entries
|
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1310,7 +1348,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1335,7 +1373,6 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Data = nil
|
|
||||||
all = append(all, entry)
|
all = append(all, entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1510,7 +1547,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1528,7 +1565,6 @@ 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 {
|
||||||
@@ -1538,7 +1574,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1629,12 +1665,11 @@ 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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1660,7 +1695,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 := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -1779,6 +1814,7 @@ 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,10 +2,8 @@ package cups
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -277,42 +275,13 @@ 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 {
|
||||||
// pkHelper failed (e.g., no polkit agent), try lpadmin as last resort.
|
return err
|
||||||
// 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 {
|
||||||
@@ -339,12 +308,6 @@ 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,8 +70,6 @@ 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))
|
||||||
}
|
}
|
||||||
@@ -466,22 +464,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
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,16 +55,6 @@ 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"`
|
||||||
@@ -87,7 +77,6 @@ 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 {
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
|
|||||||
|
|
||||||
func InitializeManager() (*Manager, error) {
|
func InitializeManager() (*Manager, error) {
|
||||||
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
||||||
return nil, fmt.Errorf("insufficient permissions to access input devices")
|
return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewManager()
|
return NewManager()
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||||
log.Warnf("Screensaver name %s already owned by another process", name)
|
log.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", name)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ 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,10 +111,6 @@ 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,9 +1,6 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"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")
|
||||||
@@ -115,19 +112,3 @@ 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,10 +18,6 @@ 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,18 +158,26 @@ 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: ssid == currentSSID && bssid == currentBSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,65 +204,6 @@ 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 {
|
||||||
@@ -514,19 +463,27 @@ 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: ssid == currentSSID,
|
Connected: isConnected,
|
||||||
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: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,19 +1019,27 @@ 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: connected && apSSID == ssid,
|
Connected: isConnected,
|
||||||
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: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
Device: name,
|
Device: name,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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"
|
||||||
@@ -41,10 +40,6 @@ 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":
|
||||||
@@ -325,42 +320,6 @@ 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,8 +6,6 @@ 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) {
|
||||||
@@ -440,43 +438,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
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,7 +15,6 @@ 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"
|
||||||
@@ -193,15 +192,6 @@ 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,7 +14,6 @@ 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"
|
||||||
@@ -26,7 +25,6 @@ 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"
|
||||||
@@ -72,7 +70,6 @@ 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
|
|
||||||
|
|
||||||
const dbusClientID = "dms-dbus-client"
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
@@ -191,7 +188,7 @@ func InitializeFreedeskManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeWaylandManager(geoClient geolocation.Client) error {
|
func InitializeWaylandManager() error {
|
||||||
log.Info("Attempting to initialize Wayland gamma control...")
|
log.Info("Attempting to initialize Wayland gamma control...")
|
||||||
|
|
||||||
if wlContext == nil {
|
if wlContext == nil {
|
||||||
@@ -204,7 +201,7 @@ func InitializeWaylandManager(geoClient geolocation.Client) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config := wayland.DefaultConfig()
|
config := wayland.DefaultConfig()
|
||||||
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
|
manager, err := wayland.NewManager(wlContext.Display(), config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to initialize wayland manager: %v", err)
|
log.Errorf("Failed to initialize wayland manager: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -385,27 +382,14 @@ func InitializeDbusManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeThemeModeManager(geoClient geolocation.Client) error {
|
func InitializeThemeModeManager() error {
|
||||||
manager := thememode.NewManager(geoClient)
|
manager := thememode.NewManager()
|
||||||
themeModeManager = manager
|
themeModeManager = manager
|
||||||
|
|
||||||
log.Info("Theme mode automation manager initialized")
|
log.Info("Theme mode automation manager initialized")
|
||||||
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()
|
||||||
|
|
||||||
@@ -553,10 +537,6 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -1327,9 +1307,6 @@ func cleanupManagers() {
|
|||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
if locationManager != nil {
|
|
||||||
locationManager.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(printDocs bool) error {
|
func Start(printDocs bool) error {
|
||||||
@@ -1511,9 +1488,6 @@ 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...")
|
||||||
@@ -1545,9 +1519,6 @@ func Start(printDocs bool) error {
|
|||||||
loginctlReady := make(chan struct{})
|
loginctlReady := make(chan struct{})
|
||||||
freedesktopReady := make(chan struct{})
|
freedesktopReady := make(chan struct{})
|
||||||
|
|
||||||
geoClient := geolocation.NewClient()
|
|
||||||
defer geoClient.Close()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(loginctlReady)
|
defer close(loginctlReady)
|
||||||
if err := InitializeLoginctlManager(); err != nil {
|
if err := InitializeLoginctlManager(); err != nil {
|
||||||
@@ -1592,7 +1563,7 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := InitializeWaylandManager(geoClient); err != nil {
|
if err := InitializeWaylandManager(); err != nil {
|
||||||
log.Warnf("Wayland manager unavailable: %v", err)
|
log.Warnf("Wayland manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1624,7 +1595,7 @@ func Start(printDocs bool) error {
|
|||||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := InitializeThemeModeManager(geoClient); err != nil {
|
if err := InitializeThemeModeManager(); err != nil {
|
||||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||||
} else {
|
} else {
|
||||||
notifyCapabilityChange()
|
notifyCapabilityChange()
|
||||||
@@ -1637,12 +1608,6 @@ func Start(printDocs bool) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := InitializeLocationManager(geoClient); err != nil {
|
|
||||||
log.Warnf("Location manager unavailable: %v", err)
|
|
||||||
} else {
|
|
||||||
notifyCapabilityChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
fatalErrChan := make(chan error, 1)
|
fatalErrChan := make(chan error, 1)
|
||||||
if wlrOutputManager != nil {
|
if wlrOutputManager != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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"
|
||||||
@@ -33,14 +32,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(geoClient geolocation.Client) *Manager {
|
func NewManager() *Manager {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
config: Config{
|
config: Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -54,7 +51,6 @@ func NewManager(geoClient geolocation.Client) *Manager {
|
|||||||
},
|
},
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
updateTrigger: make(chan struct{}, 1),
|
updateTrigger: make(chan struct{}, 1),
|
||||||
geoClient: geoClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.updateState(time.Now())
|
m.updateState(time.Now())
|
||||||
@@ -331,17 +327,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
|||||||
}
|
}
|
||||||
m.locationMutex.RUnlock()
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
location, err := m.geoClient.GetLocation()
|
lat, lon, err := wayland.FetchIPLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.locationMutex.Lock()
|
m.locationMutex.Lock()
|
||||||
m.cachedIPLat = &location.Latitude
|
m.cachedIPLat = lat
|
||||||
m.cachedIPLon = &location.Longitude
|
m.cachedIPLon = lon
|
||||||
m.locationMutex.Unlock()
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
return m.cachedIPLat, m.cachedIPLon
|
return lat, lon
|
||||||
}
|
}
|
||||||
|
|
||||||
func statesEqual(a, b *State) bool {
|
func statesEqual(a, b *State) bool {
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const animKelvinStep = 25
|
const animKelvinStep = 25
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
|
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -41,7 +40,6 @@ func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, c
|
|||||||
updateTrigger: make(chan struct{}, 1),
|
updateTrigger: make(chan struct{}, 1),
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
dbusSignal: make(chan *dbus.Signal, 16),
|
dbusSignal: make(chan *dbus.Signal, 16),
|
||||||
geoClient: geoClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.setupRegistry(); err != nil {
|
if err := m.setupRegistry(); err != nil {
|
||||||
@@ -439,16 +437,15 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
|||||||
}
|
}
|
||||||
m.locationMutex.RUnlock()
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
location, err := m.geoClient.GetLocation()
|
lat, lon, err := FetchIPLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.locationMutex.Lock()
|
m.locationMutex.Lock()
|
||||||
m.cachedIPLat = &location.Latitude
|
m.cachedIPLat = lat
|
||||||
m.cachedIPLon = &location.Longitude
|
m.cachedIPLon = lon
|
||||||
m.locationMutex.Unlock()
|
m.locationMutex.Unlock()
|
||||||
return m.cachedIPLat, m.cachedIPLon
|
return lat, lon
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -391,20 +390,18 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
func TestNewManager_GetRegistryError(t *testing.T) {
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
mockDisplay.EXPECT().Context().Return(nil)
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
||||||
|
|
||||||
config := DefaultConfig()
|
config := DefaultConfig()
|
||||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
_, err := NewManager(mockDisplay, config)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "get registry")
|
assert.Contains(t, err.Error(), "get registry")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewManager_InvalidConfig(t *testing.T) {
|
func TestNewManager_InvalidConfig(t *testing.T) {
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
|
||||||
|
|
||||||
config := Config{
|
config := Config{
|
||||||
LowTemp: 500,
|
LowTemp: 500,
|
||||||
@@ -412,6 +409,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
|||||||
Gamma: 1.0,
|
Gamma: 1.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
_, err := NewManager(mockDisplay, config)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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"
|
||||||
@@ -98,8 +97,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package wlcontext
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
"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/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -123,6 +123,9 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consecutiveErrors := 0
|
||||||
|
const maxConsecutiveErrors = 20
|
||||||
|
|
||||||
for {
|
for {
|
||||||
sc.drainCmdQueue()
|
sc.drainCmdQueue()
|
||||||
|
|
||||||
@@ -153,9 +156,19 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
||||||
log.Errorf("Wayland connection error: %v", err)
|
consecutiveErrors++
|
||||||
return
|
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
|
||||||
|
|
||||||
|
if consecutiveErrors >= maxConsecutiveErrors {
|
||||||
|
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consecutiveErrors = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
|||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
cmdq: make(chan cmd, 128),
|
cmdq: make(chan cmd, 512),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
fatalError: make(chan error, 1),
|
fatalError: make(chan error, 1),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -42,6 +43,9 @@ type Model struct {
|
|||||||
sudoPassword string
|
sudoPassword string
|
||||||
existingConfigs []ExistingConfigInfo
|
existingConfigs []ExistingConfigInfo
|
||||||
fingerprintFailed bool
|
fingerprintFailed bool
|
||||||
|
|
||||||
|
availablePrivesc []privesc.Tool
|
||||||
|
selectedPrivesc int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(version string, logFilePath string) Model {
|
func NewModel(version string, logFilePath string) Model {
|
||||||
@@ -147,6 +151,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.updateGentooUseFlagsState(msg)
|
return m.updateGentooUseFlagsState(msg)
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.updateGentooGCCCheckState(msg)
|
return m.updateGentooGCCCheckState(msg)
|
||||||
|
case StateSelectPrivesc:
|
||||||
|
return m.updateSelectPrivescState(msg)
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.updateAuthMethodChoiceState(msg)
|
return m.updateAuthMethodChoiceState(msg)
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
@@ -189,6 +195,8 @@ func (m Model) View() string {
|
|||||||
return m.viewGentooUseFlags()
|
return m.viewGentooUseFlags()
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.viewGentooGCCCheck()
|
return m.viewGentooGCCCheck()
|
||||||
|
case StateSelectPrivesc:
|
||||||
|
return m.viewSelectPrivesc()
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.viewAuthMethodChoice()
|
return m.viewAuthMethodChoice()
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const (
|
|||||||
StateDependencyReview
|
StateDependencyReview
|
||||||
StateGentooUseFlags
|
StateGentooUseFlags
|
||||||
StateGentooGCCCheck
|
StateGentooGCCCheck
|
||||||
|
StateSelectPrivesc
|
||||||
StateAuthMethodChoice
|
StateAuthMethodChoice
|
||||||
StateFingerprintAuth
|
StateFingerprintAuth
|
||||||
StatePasswordPrompt
|
StatePasswordPrompt
|
||||||
|
|||||||
@@ -180,16 +180,7 @@ func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if fingerprint is enabled
|
return m.enterAuthPhase()
|
||||||
if checkFingerprintEnabled() {
|
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0 // Default to fingerprint
|
|
||||||
return m, nil
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateSelectWindowManager
|
m.state = StateSelectWindowManager
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -56,14 +56,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.state = StateGentooGCCCheck
|
m.state = StateGentooGCCCheck
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if checkFingerprintEnabled() {
|
return m.enterAuthPhase()
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||||
@@ -75,14 +68,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.selectedWM == 1 {
|
if m.selectedWM == 1 {
|
||||||
return m, m.checkGCCVersion()
|
return m, m.checkGCCVersion()
|
||||||
}
|
}
|
||||||
if checkFingerprintEnabled() {
|
return m.enterAuthPhase()
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateDependencyReview
|
m.state = StateDependencyReview
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -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-bin"
|
return "dms-shell"
|
||||||
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"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -274,8 +275,7 @@ func (m Model) delayThenReturn() tea.Cmd {
|
|||||||
|
|
||||||
func (m Model) tryFingerprint() tea.Cmd {
|
func (m Model) tryFingerprint() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
clearCmd := exec.Command("sudo", "-k")
|
_ = privesc.ClearCache(context.Background())
|
||||||
clearCmd.Run()
|
|
||||||
|
|
||||||
tmpDir := os.TempDir()
|
tmpDir := os.TempDir()
|
||||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||||
@@ -289,15 +289,9 @@ func (m Model) tryFingerprint() tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
if err := privesc.ValidateWithAskpass(ctx, askpassScript); err != nil {
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
return passwordValidMsg{password: "", valid: true}
|
return passwordValidMsg{password: "", valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,32 +301,9 @@ func (m Model) validatePassword(password string) tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
if err := privesc.ValidatePassword(ctx, password); err != nil {
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(stdin, "%s\n", password)
|
|
||||||
stdin.Close()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return passwordValidMsg{password: password, valid: true}
|
return passwordValidMsg{password: password, valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) viewSelectPrivesc() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString(m.renderBanner())
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.styles.Title.Render("Privilege Escalation Tool"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(m.styles.Normal.Render("Multiple privilege tools are available. Choose one for installation:"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
for i, t := range m.availablePrivesc {
|
||||||
|
label := fmt.Sprintf("%s — %s", t.Name(), privescToolDescription(t))
|
||||||
|
switch i {
|
||||||
|
case m.selectedPrivesc:
|
||||||
|
b.WriteString(m.styles.SelectedOption.Render("▶ " + label))
|
||||||
|
default:
|
||||||
|
b.WriteString(m.styles.Normal.Render(" " + label))
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Set %s=<tool> to skip this prompt in future runs.", privesc.EnvVar)))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back"))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) updateSelectPrivescState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
keyMsg, ok := msg.(tea.KeyMsg)
|
||||||
|
if !ok {
|
||||||
|
return m, m.listenForLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keyMsg.String() {
|
||||||
|
case "up":
|
||||||
|
if m.selectedPrivesc > 0 {
|
||||||
|
m.selectedPrivesc--
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
if m.selectedPrivesc < len(m.availablePrivesc)-1 {
|
||||||
|
m.selectedPrivesc++
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
chosen := m.availablePrivesc[m.selectedPrivesc]
|
||||||
|
if err := privesc.SetTool(chosen); err != nil {
|
||||||
|
m.err = fmt.Errorf("failed to select %s: %w", chosen.Name(), err)
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m.routeToAuthAfterPrivesc()
|
||||||
|
case "esc":
|
||||||
|
m.state = StateDependencyReview
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privescToolDescription(t privesc.Tool) string {
|
||||||
|
switch t {
|
||||||
|
case privesc.ToolSudo:
|
||||||
|
return "classic sudo (supports password prompt in this installer)"
|
||||||
|
case privesc.ToolDoas:
|
||||||
|
return "OpenBSD-style doas (requires persist or nopass in /etc/doas.conf)"
|
||||||
|
case privesc.ToolRun0:
|
||||||
|
return "systemd run0 (authenticated via polkit)"
|
||||||
|
default:
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeToAuthAfterPrivesc advances from the privesc-selection screen to the
|
||||||
|
// right auth flow. Sudo goes through the fingerprint/password path; doas and
|
||||||
|
// run0 skip password entry and proceed to install.
|
||||||
|
func (m Model) routeToAuthAfterPrivesc() (tea.Model, tea.Cmd) {
|
||||||
|
tool, err := privesc.Detect()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tool == privesc.ToolSudo {
|
||||||
|
if checkFingerprintEnabled() {
|
||||||
|
m.state = StateAuthMethodChoice
|
||||||
|
m.selectedConfig = 0
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.state = StatePasswordPrompt
|
||||||
|
m.passwordInput.Focus()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sudoPassword = ""
|
||||||
|
m.packageProgress = packageInstallProgressMsg{}
|
||||||
|
m.state = StateInstallingPackages
|
||||||
|
m.isLoading = true
|
||||||
|
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterAuthPhase is called when dependency review (or the Gentoo screens)
|
||||||
|
// finish. It either routes directly to the sudo/fingerprint flow or shows
|
||||||
|
// the privesc-tool selection screen when multiple tools are available and
|
||||||
|
// no $DMS_PRIVESC override is set.
|
||||||
|
func (m Model) enterAuthPhase() (tea.Model, tea.Cmd) {
|
||||||
|
tools := privesc.AvailableTools()
|
||||||
|
_, envSet := privesc.EnvOverride()
|
||||||
|
|
||||||
|
if len(tools) == 0 {
|
||||||
|
m.err = fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if envSet || len(tools) == 1 {
|
||||||
|
return m.routeToAuthAfterPrivesc()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.availablePrivesc = tools
|
||||||
|
m.selectedPrivesc = 0
|
||||||
|
m.state = StateSelectPrivesc
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
# Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
|
||||||
GO_INSTALLED=$$(go version | grep -oP 'go\K[0-9]+\.[0-9]+'); \
|
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
|
||||||
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 (single shell to preserve variables; arch: Debian amd64/arm64 -> Makefile amd64/arm64)
|
# Build dms-cli from source using vendored dependencies
|
||||||
|
# Extract version info and build in single shell to preserve variables
|
||||||
|
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
|
||||||
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.3/dms-qml.tar.gz</param>
|
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/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,5 +1,6 @@
|
|||||||
dms-greeter (1.4.3db1) unstable; urgency=medium
|
dms-greeter (1.4.2db8) unstable; urgency=medium
|
||||||
|
|
||||||
* Update to v1.4.3 stable release
|
* Initial Debian OBS package
|
||||||
|
* Port from Ubuntu/Fedora packaging
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000
|
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
|||||||
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
|
||||||
Package: dms
|
Package: dms
|
||||||
Architecture: amd64
|
Architecture: amd64 arm64
|
||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
dms-distropkg-amd64.gz
|
dms-distropkg-amd64.gz
|
||||||
|
dms-distropkg-arm64.gz
|
||||||
dms-source.tar.gz
|
dms-source.tar.gz
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Include files that are normally excluded by .gitignore
|
# Include files that are normally excluded by .gitignore
|
||||||
# These are needed for the build process on Launchpad
|
# These are needed for the build process on Launchpad
|
||||||
tar-ignore = !dms-distropkg-amd64.gz
|
tar-ignore = !dms-distropkg-amd64.gz
|
||||||
|
tar-ignore = !dms-distropkg-arm64.gz
|
||||||
tar-ignore = !dms-source.tar.gz
|
tar-ignore = !dms-source.tar.gz
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
%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.25.7
|
|
||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Epoch: 2
|
Epoch: 2
|
||||||
@@ -15,12 +14,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
|
||||||
@@ -29,7 +28,7 @@ Requires: accountsservice
|
|||||||
Requires: dms-cli = %{epoch}:%{version}-%{release}
|
Requires: dms-cli = %{epoch}:%{version}-%{release}
|
||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
# Core utilities (Recommended for DMS functionality)
|
# Core utilities (Highly recommended for DMS functionality)
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
@@ -67,28 +66,6 @@ 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"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user