mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 }}
|
||||||
|
|||||||
+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"
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"policy_version": 1,
|
|
||||||
"blocked_commands": [
|
|
||||||
"greeter install",
|
|
||||||
"greeter enable",
|
|
||||||
"greeter sync",
|
|
||||||
"greeter uninstall",
|
|
||||||
"setup"
|
|
||||||
],
|
|
||||||
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
|
||||||
}
|
|
||||||
@@ -222,19 +222,16 @@ func init() {
|
|||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
copyFromStdin := false
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) > 0:
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
case clipCopyDownload || clipCopyType == "__multi__":
|
default:
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("read stdin: %v", err)
|
log.Fatalf("read stdin: %v", err)
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
copyFromStdin = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipCopyDownload {
|
if clipCopyDownload {
|
||||||
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if copyFromStdin {
|
|
||||||
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("copy: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-1109
File diff suppressed because it is too large
Load Diff
@@ -16,10 +16,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
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,
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
|
||||||
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
immutablePolicyOnce sync.Once
|
|
||||||
immutablePolicy immutableCommandPolicy
|
|
||||||
immutablePolicyErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/cli-policy.default.json
|
|
||||||
var defaultCLIPolicyJSON []byte
|
|
||||||
|
|
||||||
type immutableCommandPolicy struct {
|
|
||||||
ImmutableSystem bool
|
|
||||||
ImmutableReason string
|
|
||||||
BlockedCommands []string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliPolicyFile struct {
|
|
||||||
PolicyVersion int `json:"policy_version"`
|
|
||||||
ImmutableSystem *bool `json:"immutable_system"`
|
|
||||||
BlockedCommands *[]string `json:"blocked_commands"`
|
|
||||||
Message *string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCommandSpec(raw string) string {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
|
||||||
normalized = strings.TrimPrefix(normalized, "dms ")
|
|
||||||
return strings.Join(strings.Fields(normalized), " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeBlockedCommands(raw []string) []string {
|
|
||||||
normalized := make([]string, 0, len(raw))
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, cmd := range raw {
|
|
||||||
spec := normalizeCommandSpec(cmd)
|
|
||||||
if spec == "" || seen[spec] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[spec] = true
|
|
||||||
normalized = append(normalized, spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
|
||||||
normalizedPath := normalizeCommandSpec(commandPath)
|
|
||||||
if normalizedPath == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range blocked {
|
|
||||||
spec := normalizeCommandSpec(entry)
|
|
||||||
if spec == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var policy cliPolicyFile
|
|
||||||
if err := json.Unmarshal(data, &policy); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &policy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
|
||||||
policyFile, err := loadPolicyFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if policyFile == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if policyFile.ImmutableSystem != nil {
|
|
||||||
base.ImmutableSystem = *policyFile.ImmutableSystem
|
|
||||||
}
|
|
||||||
if policyFile.BlockedCommands != nil {
|
|
||||||
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
|
||||||
}
|
|
||||||
if policyFile.Message != nil {
|
|
||||||
msg := strings.TrimSpace(*policyFile.Message)
|
|
||||||
if msg != "" {
|
|
||||||
base.Message = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOSReleaseMap(path string) map[string]string {
|
|
||||||
values := make(map[string]string)
|
|
||||||
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
|
||||||
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
|
||||||
values[key] = strings.ToLower(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasAnyToken(text string, tokens ...string) bool {
|
|
||||||
if text == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.Contains(text, token) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectImmutableSystem() (bool, string) {
|
|
||||||
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
|
||||||
return true, "/run/ostree-booted is present"
|
|
||||||
}
|
|
||||||
|
|
||||||
osRelease := readOSReleaseMap("/etc/os-release")
|
|
||||||
if len(osRelease) == 0 {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
id := osRelease["ID"]
|
|
||||||
idLike := osRelease["ID_LIKE"]
|
|
||||||
variantID := osRelease["VARIANT_ID"]
|
|
||||||
name := osRelease["NAME"]
|
|
||||||
prettyName := osRelease["PRETTY_NAME"]
|
|
||||||
|
|
||||||
immutableIDs := map[string]bool{
|
|
||||||
"bluefin": true,
|
|
||||||
"bazzite": true,
|
|
||||||
"silverblue": true,
|
|
||||||
"kinoite": true,
|
|
||||||
"sericea": true,
|
|
||||||
"onyx": true,
|
|
||||||
"aurora": true,
|
|
||||||
"fedora-iot": true,
|
|
||||||
"fedora-coreos": true,
|
|
||||||
}
|
|
||||||
if immutableIDs[id] {
|
|
||||||
return true, "os-release ID=" + id
|
|
||||||
}
|
|
||||||
|
|
||||||
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
|
||||||
if hasAnyToken(variantID, markers...) {
|
|
||||||
return true, "os-release VARIANT_ID=" + variantID
|
|
||||||
}
|
|
||||||
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
|
||||||
return true, "os-release ID_LIKE=" + idLike
|
|
||||||
}
|
|
||||||
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
|
||||||
return true, "os-release identifies an atomic/ostree variant"
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
|
||||||
immutablePolicyOnce.Do(func() {
|
|
||||||
detectedImmutable, reason := detectImmutableSystem()
|
|
||||||
immutablePolicy = immutableCommandPolicy{
|
|
||||||
ImmutableSystem: detectedImmutable,
|
|
||||||
ImmutableReason: reason,
|
|
||||||
BlockedCommands: []string{"greeter install", "greeter enable", "greeter sync", "setup"},
|
|
||||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultPolicy cliPolicyFile
|
|
||||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
|
||||||
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if defaultPolicy.BlockedCommands != nil {
|
|
||||||
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
|
||||||
}
|
|
||||||
if defaultPolicy.Message != nil {
|
|
||||||
msg := strings.TrimSpace(*defaultPolicy.Message)
|
|
||||||
if msg != "" {
|
|
||||||
immutablePolicy.Message = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
|
||||||
immutablePolicyErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
|
||||||
immutablePolicyErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if immutablePolicyErr != nil {
|
|
||||||
return nil, immutablePolicyErr
|
|
||||||
}
|
|
||||||
return &immutablePolicy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
|
||||||
policy, err := getImmutablePolicy()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !policy.ImmutableSystem {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
|
||||||
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reason := ""
|
|
||||||
if policy.ImmutableReason != "" {
|
|
||||||
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
|
||||||
}
|
|
||||||
+10
-1
@@ -16,10 +16,19 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
// Add subcommands to greeter
|
||||||
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to setup
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
|
// Add subcommands to update
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|||||||
@@ -11,20 +11,29 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// Add flags
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
// Add subcommands to greeter
|
||||||
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to setup
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Block root
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func findCommandPath(cmd string) (string, error) {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
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=
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
@@ -14,37 +12,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
return CopyOpts(data, mimeType, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
total := 0
|
|
||||||
for total < len(data) {
|
|
||||||
n, err := writer.Write(data[total:])
|
|
||||||
total += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if total != len(data) {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
if !foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServeReader(data, mimeType, pasteOnce)
|
return copyServe(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
args = append(args, "--paste-once")
|
args = append(args, "--paste-once")
|
||||||
@@ -52,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
args = append(args, "--type", mimeType)
|
args = append(args, "--type", mimeType)
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
if stdinSource, ok := data.(*os.File); ok {
|
|
||||||
cmd.Stdin = stdinSource
|
|
||||||
return cmd.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
@@ -70,66 +44,16 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := io.Copy(stdin, data); err != nil {
|
if _, err := stdin.Write(data); err != nil {
|
||||||
stdin.Close()
|
stdin.Close()
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
}
|
}
|
||||||
if err := stdin.Close(); err != nil {
|
stdin.Close()
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
cachedData, err := createClipboardCacheFile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(cachedData.Name())
|
|
||||||
|
|
||||||
if _, err := io.Copy(cachedData, data); err != nil {
|
|
||||||
return fmt.Errorf("cache clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
if err := cachedData.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
cachedFile, err := os.Open(cachedData.Name())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer cachedFile.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
|
||||||
return fmt.Errorf("write clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
|
||||||
preferredDirs := []string{}
|
|
||||||
|
|
||||||
if cacheDir, err := os.UserCacheDir(); err == nil {
|
|
||||||
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
|
|
||||||
}
|
|
||||||
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
|
|
||||||
|
|
||||||
for _, dir := range preferredDirs {
|
|
||||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
|
|
||||||
if err == nil {
|
|
||||||
return cachedData, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.CreateTemp("", "dms-clipboard-*")
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -215,18 +139,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)
|
defer syscall.Close(e.Fd)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := writeTo(file); err != nil {
|
file.Write(data)
|
||||||
select {
|
|
||||||
case sendErr <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
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
|
||||||
|
|||||||
@@ -440,10 +440,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
|
|||||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
hasNiri := false
|
hasNiri := false
|
||||||
|
hasQuickshell := false
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "niri-git" {
|
if pkg == "niri-git" {
|
||||||
hasNiri = true
|
hasNiri = true
|
||||||
}
|
}
|
||||||
|
if pkg == "quickshell" || pkg == "quickshell-git" {
|
||||||
|
hasQuickshell = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If quickshell is in the list, always reinstall google-breakpad first
|
||||||
|
if hasQuickshell {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.63,
|
||||||
|
Step: "Reinstalling google-breakpad for quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
||||||
|
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
@@ -597,16 +616,10 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||||
if pkg == "dms-shell-bin" {
|
// since we manually manage those dependencies
|
||||||
progressChan <- InstallProgressMsg{
|
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||||
Phase: PhaseAURPackages,
|
// Pre-install dependencies from .SRCINFO
|
||||||
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),
|
||||||
@@ -615,19 +628,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies from .SRCINFO
|
// Install dependencies and makedepends explicitly
|
||||||
depFilter := ""
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
if pkg == "dms-shell-git" {
|
|
||||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
|
||||||
}
|
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
fmt.Sprintf(`
|
fmt.Sprintf(`
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
if [[ "%s" == *"quickshell"* ]]; then
|
||||||
|
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
||||||
|
fi
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||||
fi
|
fi
|
||||||
`, srcinfoPath, depFilter, sudoPassword))
|
`, srcinfoPath, pkg, sudoPassword))
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||||
@@ -644,6 +657,14 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -656,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
buildCmd.Dir = packageDir
|
buildCmd.Dir = packageDir
|
||||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
||||||
|
|
||||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -92,25 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
return debianPackageInstalledPrecisely(pkg)
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
}
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
func debianPackageInstalledPrecisely(pkg string) bool {
|
|
||||||
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(output)) == "installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsString(values []string, target string) bool {
|
|
||||||
for _, value := range values {
|
|
||||||
if value == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -210,12 +194,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: "Installing development dependencies...",
|
Step: "Installing development dependencies...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -395,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-install-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
@@ -506,46 +482,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
totalGroups := len(groups)
|
args = append(args, packages...)
|
||||||
|
|
||||||
groupIndex := 0
|
progressChan <- InstallProgressMsg{
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
Phase: PhaseSystemPackages,
|
||||||
if len(groupPackages) == 0 {
|
Progress: 0.40,
|
||||||
return nil
|
Step: "Installing system packages...",
|
||||||
}
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
groupIndex++
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
startProgress := 0.40
|
|
||||||
endProgress := 0.60
|
|
||||||
if totalGroups > 1 {
|
|
||||||
if groupIndex == 1 {
|
|
||||||
endProgress = 0.50
|
|
||||||
} else {
|
|
||||||
startProgress = 0.50
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := d.aptInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: startProgress,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -484,7 +484,28 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -494,57 +515,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
if minimal {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return append(args, packages...)
|
|
||||||
}
|
args = append(args, packages...)
|
||||||
|
|
||||||
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
progressChan <- InstallProgressMsg{
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
Phase: PhaseAURPackages,
|
||||||
totalGroups := len(groups)
|
Progress: 0.70,
|
||||||
|
Step: "Installing COPR packages...",
|
||||||
groupIndex := 0
|
IsComplete: false,
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
NeedsSudo: true,
|
||||||
if len(groupPackages) == 0 {
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
groupIndex++
|
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := f.dnfInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range groups {
|
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package distros
|
|
||||||
|
|
||||||
type minimalInstallGroup struct {
|
|
||||||
packages []string
|
|
||||||
minimal bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldPreferMinimalInstall(pkg string) bool {
|
|
||||||
switch pkg {
|
|
||||||
case "niri", "niri-git":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if shouldPreferMinimalInstall(pkg) {
|
|
||||||
minimal = append(minimal, pkg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
normal = append(normal, pkg)
|
|
||||||
}
|
|
||||||
return normal, minimal
|
|
||||||
}
|
|
||||||
|
|
||||||
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
|
||||||
normal, minimal := splitMinimalInstallPackages(packages)
|
|
||||||
groups := make([]minimalInstallGroup, 0, 2)
|
|
||||||
if len(minimal) > 0 {
|
|
||||||
groups = append(groups, minimalInstallGroup{
|
|
||||||
packages: minimal,
|
|
||||||
minimal: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(normal) > 0 {
|
|
||||||
groups = append(groups, minimalInstallGroup{
|
|
||||||
packages: normal,
|
|
||||||
minimal: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
@@ -29,8 +29,6 @@ type OpenSUSEDistribution struct {
|
|||||||
config DistroConfig
|
config DistroConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
|
||||||
|
|
||||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
base := NewBaseDistribution(logChan)
|
base := NewBaseDistribution(logChan)
|
||||||
return &OpenSUSEDistribution{
|
return &OpenSUSEDistribution{
|
||||||
@@ -201,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
return []string{}
|
return []string{
|
||||||
|
"make",
|
||||||
|
"unzip",
|
||||||
|
"gcc",
|
||||||
|
"gcc-c++",
|
||||||
|
"cmake",
|
||||||
|
"ninja",
|
||||||
|
"pkgconf-pkg-config",
|
||||||
|
"git",
|
||||||
|
"qt6-base-devel",
|
||||||
|
"qt6-declarative-devel",
|
||||||
|
"qt6-declarative-private-devel",
|
||||||
|
"qt6-shadertools",
|
||||||
|
"qt6-shadertools-devel",
|
||||||
|
"qt6-wayland-devel",
|
||||||
|
"qt6-waylandclient-private-devel",
|
||||||
|
"spirv-tools-devel",
|
||||||
|
"cli11-devel",
|
||||||
|
"wayland-protocols-devel",
|
||||||
|
"libgbm-devel",
|
||||||
|
"libdrm-devel",
|
||||||
|
"pipewire-devel",
|
||||||
|
"jemalloc-devel",
|
||||||
|
"wayland-utils",
|
||||||
|
"Mesa-libGLESv3-devel",
|
||||||
|
"pam-devel",
|
||||||
|
"glib2-devel",
|
||||||
|
"polkit-devel",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -271,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting prerequisite check...",
|
LogOutput: "Starting prerequisite check...",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
@@ -305,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
|
||||||
|
|
||||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
|
||||||
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{openSUSENiriWaylandServerPackage}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
|
||||||
for _, pkg := range extraPkgs {
|
|
||||||
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
|
||||||
systemPkgs = append(systemPkgs, pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemPkgs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
names := make([]string, len(packages))
|
names := make([]string, len(packages))
|
||||||
for i, pkg := range packages {
|
for i, pkg := range packages {
|
||||||
@@ -515,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenSUSEInstallMediaURI(uri string) bool {
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
|
||||||
|
|
||||||
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "hd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "iso:/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseZypperInstallMediaAliases(output string) []string {
|
|
||||||
var aliases []string
|
|
||||||
|
|
||||||
for _, line := range strings.Split(output, "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || !strings.Contains(line, "|") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(line, "|")
|
|
||||||
if len(parts) < 7 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range parts {
|
|
||||||
parts[i] = strings.TrimSpace(parts[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
alias := parts[1]
|
|
||||||
enabled := strings.ToLower(parts[3])
|
|
||||||
uri := parts[len(parts)-1]
|
|
||||||
|
|
||||||
if alias == "" || strings.EqualFold(alias, "alias") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if enabled != "" && enabled != "yes" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !isOpenSUSEInstallMediaURI(uri) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases = append(aliases, alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
return aliases
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
|
||||||
output, err := listCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
|
||||||
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases := parseZypperInstallMediaAliases(string(output))
|
|
||||||
if len(aliases) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhasePrerequisites,
|
|
||||||
Progress: 0.055,
|
|
||||||
Step: "Disabling install media repositories...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
|
||||||
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, alias := range aliases {
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
|
||||||
repoOutput, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
|
||||||
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
|
||||||
}
|
|
||||||
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
args := []string{"zypper", "install", "-y"}
|
||||||
totalGroups := len(groups)
|
args = append(args, packages...)
|
||||||
|
|
||||||
groupIndex := 0
|
progressChan <- InstallProgressMsg{
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
Phase: PhaseSystemPackages,
|
||||||
if len(groupPackages) == 0 {
|
Progress: 0.40,
|
||||||
return nil
|
Step: "Installing system packages...",
|
||||||
}
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
groupIndex++
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := o.zypperInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
return debianPackageInstalledPrecisely(pkg)
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -452,7 +454,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -461,59 +477,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
args = append(args, packages...)
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-install-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
progressChan <- InstallProgressMsg{
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
Phase: PhaseAURPackages,
|
||||||
totalGroups := len(groups)
|
Progress: 0.70,
|
||||||
|
Step: "Installing PPA packages...",
|
||||||
groupIndex := 0
|
IsComplete: false,
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
NeedsSudo: true,
|
||||||
if len(groupPackages) == 0 {
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
groupIndex++
|
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := u.aptInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# AppArmor profile for dms-greeter
|
|
||||||
#
|
|
||||||
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
|
||||||
# Manual edits will be overwritten on next sync.
|
|
||||||
#
|
|
||||||
# Mode: complain (denials are logged, nothing is blocked)
|
|
||||||
# To switch to enforce after validating with `aa-logprof`:
|
|
||||||
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
|
||||||
#
|
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
|
||||||
#include <abstractions/base>
|
|
||||||
#include <abstractions/bash>
|
|
||||||
|
|
||||||
# The launcher script itself
|
|
||||||
/usr/bin/dms-greeter r,
|
|
||||||
|
|
||||||
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
|
||||||
/var/cache/dms-greeter/ rw,
|
|
||||||
/var/cache/dms-greeter/** rwlk,
|
|
||||||
|
|
||||||
# DMS config — packaged path
|
|
||||||
/usr/share/quickshell/dms-greeter/ r,
|
|
||||||
/usr/share/quickshell/dms-greeter/** r,
|
|
||||||
/usr/share/quickshell/ r,
|
|
||||||
/usr/share/quickshell/** r,
|
|
||||||
|
|
||||||
# DMS config — system and user overrides
|
|
||||||
/etc/dms/ r,
|
|
||||||
/etc/dms/** r,
|
|
||||||
/usr/share/dms/ r,
|
|
||||||
/usr/share/dms/** r,
|
|
||||||
/home/*/.config/quickshell/ r,
|
|
||||||
/home/*/.config/quickshell/** r,
|
|
||||||
/root/.config/quickshell/ r,
|
|
||||||
/root/.config/quickshell/** r,
|
|
||||||
|
|
||||||
# greetd / PAM — read-only for session setup
|
|
||||||
/etc/greetd/ r,
|
|
||||||
/etc/greetd/** r,
|
|
||||||
/etc/pam.d/ r,
|
|
||||||
/etc/pam.d/** r,
|
|
||||||
/usr/lib/pam.d/ r,
|
|
||||||
/usr/lib/pam.d/** r,
|
|
||||||
|
|
||||||
# Compositor binaries — run unconfined so each compositor uses its own profile
|
|
||||||
/usr/bin/niri Ux,
|
|
||||||
/usr/bin/hyprland Ux,
|
|
||||||
/usr/bin/Hyprland Ux,
|
|
||||||
/usr/bin/sway Ux,
|
|
||||||
/usr/bin/labwc Ux,
|
|
||||||
/usr/bin/scroll Ux,
|
|
||||||
/usr/bin/miracle-wm Ux,
|
|
||||||
/usr/bin/mango Ux,
|
|
||||||
|
|
||||||
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
|
||||||
/usr/bin/qs Ux,
|
|
||||||
/usr/bin/quickshell Ux,
|
|
||||||
|
|
||||||
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
|
||||||
/run/user/[0-9]*/ rw,
|
|
||||||
/run/user/[0-9]*/** rw,
|
|
||||||
|
|
||||||
# DRM / GPU devices (required for Wayland compositor startup)
|
|
||||||
/dev/dri/ r,
|
|
||||||
/dev/dri/* rw,
|
|
||||||
/dev/udmabuf rw,
|
|
||||||
|
|
||||||
# Input devices
|
|
||||||
/dev/input/ r,
|
|
||||||
/dev/input/* r,
|
|
||||||
|
|
||||||
# Systemd journal / logging
|
|
||||||
/run/systemd/journal/socket rw,
|
|
||||||
/dev/log rw,
|
|
||||||
|
|
||||||
# Shell helper binaries invoked by the launcher script
|
|
||||||
/usr/bin/env ix,
|
|
||||||
/usr/bin/mkdir ix,
|
|
||||||
/usr/bin/cat ix,
|
|
||||||
/usr/bin/grep ix,
|
|
||||||
/usr/bin/dirname ix,
|
|
||||||
/usr/bin/basename ix,
|
|
||||||
/usr/bin/command ix,
|
|
||||||
/bin/env ix,
|
|
||||||
/bin/mkdir ix,
|
|
||||||
|
|
||||||
# Signal management (compositor lifecycle)
|
|
||||||
signal (send, receive) set=("term", "int", "hup", "kill"),
|
|
||||||
}
|
|
||||||
+112
-1144
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,6 @@ 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"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,5 @@ const (
|
|||||||
dbusPath = "/org/freedesktop/login1"
|
dbusPath = "/org/freedesktop/login1"
|
||||||
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
||||||
dbusSessionInterface = "org.freedesktop.login1.Session"
|
dbusSessionInterface = "org.freedesktop.login1.Session"
|
||||||
dbusUserInterface = "org.freedesktop.login1.User"
|
|
||||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ func NewManager() (*Manager, error) {
|
|||||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionID := os.Getenv("XDG_SESSION_ID")
|
||||||
|
if sessionID == "" {
|
||||||
|
sessionID = "self"
|
||||||
|
}
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
state: &SessionState{},
|
state: &SessionState{
|
||||||
|
SessionID: sessionID,
|
||||||
|
},
|
||||||
stateMutex: sync.RWMutex{},
|
stateMutex: sync.RWMutex{},
|
||||||
|
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
@@ -53,13 +60,12 @@ func (m *Manager) initialize() error {
|
|||||||
|
|
||||||
m.initializeFallbackDelay()
|
m.initializeFallbackDelay()
|
||||||
|
|
||||||
sessionID, sessionPath, err := m.discoverSession()
|
sessionPath, err := m.getSession(m.state.SessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get session path: %w", err)
|
return fmt.Errorf("failed to get session path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.SessionID = sessionID
|
|
||||||
m.state.SessionPath = string(sessionPath)
|
m.state.SessionPath = string(sessionPath)
|
||||||
m.sessionPath = sessionPath
|
m.sessionPath = sessionPath
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
@@ -73,41 +79,6 @@ func (m *Manager) initialize() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
|
|
||||||
// 1. Explicit XDG_SESSION_ID
|
|
||||||
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
|
|
||||||
if path, err := m.getSession(id); err == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
|
|
||||||
return id, path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. PID-based lookup (works when caller is inside a session cgroup)
|
|
||||||
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
|
|
||||||
return id, path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. User's primary display session (handles UWSM and similar)
|
|
||||||
if id, path, err := m.getUserDisplaySession(); err == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
|
|
||||||
return id, path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Score all sessions for current UID
|
|
||||||
if id, path, err := m.findBestSession(); err == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
|
|
||||||
return id, path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Last resort: "self"
|
|
||||||
path, err := m.getSession("self")
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("%w", err)
|
|
||||||
}
|
|
||||||
return "self", path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
||||||
var out dbus.ObjectPath
|
var out dbus.ObjectPath
|
||||||
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
||||||
@@ -117,166 +88,6 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
|
|
||||||
var path dbus.ObjectPath
|
|
||||||
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionObj := m.conn.Object(dbusDest, path)
|
|
||||||
var id dbus.Variant
|
|
||||||
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return id.Value().(string), path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
|
|
||||||
uid := uint32(os.Getuid())
|
|
||||||
|
|
||||||
var userPath dbus.ObjectPath
|
|
||||||
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
userObj := m.conn.Object(dbusDest, userPath)
|
|
||||||
var display dbus.Variant
|
|
||||||
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
pair, ok := display.Value().([]any)
|
|
||||||
if !ok || len(pair) < 2 {
|
|
||||||
return "", "", fmt.Errorf("unexpected Display format")
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID, _ := pair[0].(string)
|
|
||||||
sessionPath, _ := pair[1].(dbus.ObjectPath)
|
|
||||||
if sessionID == "" || sessionPath == "" {
|
|
||||||
return "", "", fmt.Errorf("empty Display session")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionID, sessionPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sessionCandidate struct {
|
|
||||||
id string
|
|
||||||
path dbus.ObjectPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
|
|
||||||
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
|
|
||||||
var raw [][]any
|
|
||||||
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
uid := uint32(os.Getuid())
|
|
||||||
var candidates []sessionCandidate
|
|
||||||
for _, entry := range raw {
|
|
||||||
if len(entry) < 5 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
entryUID, _ := entry[1].(uint32)
|
|
||||||
if entryUID != uid {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
id, _ := entry[0].(string)
|
|
||||||
path, _ := entry[4].(dbus.ObjectPath)
|
|
||||||
if id != "" && path != "" {
|
|
||||||
candidates = append(candidates, sessionCandidate{id: id, path: path})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
return "", "", fmt.Errorf("no sessions for uid %d", uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
bestScore := -1
|
|
||||||
var best sessionCandidate
|
|
||||||
for _, c := range candidates {
|
|
||||||
score := m.scoreSession(c.path)
|
|
||||||
if score > bestScore {
|
|
||||||
bestScore = score
|
|
||||||
best = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bestScore < 0 {
|
|
||||||
return "", "", fmt.Errorf("no viable session found")
|
|
||||||
}
|
|
||||||
return best.id, best.path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
|
|
||||||
obj := m.conn.Object(dbusDest, path)
|
|
||||||
var props map[string]dbus.Variant
|
|
||||||
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
getStr := func(key string) string {
|
|
||||||
if v, ok := props[key]; ok {
|
|
||||||
if s, ok := v.Value().(string); ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
getBool := func(key string) bool {
|
|
||||||
if v, ok := props[key]; ok {
|
|
||||||
if b, ok := v.Value().(bool); ok {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
getUint32 := func(key string) uint32 {
|
|
||||||
if v, ok := props[key]; ok {
|
|
||||||
if u, ok := v.Value().(uint32); ok {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
class := getStr("Class")
|
|
||||||
if class != "user" {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if getBool("Remote") {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
score := 0
|
|
||||||
|
|
||||||
if getBool("Active") {
|
|
||||||
score += 100
|
|
||||||
}
|
|
||||||
|
|
||||||
switch getStr("Type") {
|
|
||||||
case "wayland", "x11":
|
|
||||||
score += 80
|
|
||||||
case "tty":
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := props["Seat"]; ok {
|
|
||||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
|
||||||
if seat, ok := seatArr[0].(string); ok && seat != "" {
|
|
||||||
score += 40
|
|
||||||
if seat == "seat0" {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if getUint32("VTNr") > 0 {
|
|
||||||
score += 20
|
|
||||||
}
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) refreshSessionBinding() error {
|
func (m *Manager) refreshSessionBinding() error {
|
||||||
if m.managerObj == nil || m.conn == nil {
|
if m.managerObj == nil || m.conn == nil {
|
||||||
return fmt.Errorf("manager not fully initialized")
|
return fmt.Errorf("manager not fully initialized")
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,65 +196,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 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
|
||||||
t.Skipf("system D-Bus unavailable: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.NotNil(t, result) {
|
assert.NotNil(t, result)
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Suggests: niri | hyprland | sway
|
Recommends: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
dmsPkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -9,7 +10,7 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = [
|
packages = [
|
||||||
cfg.package
|
dmsPkgs.dms-shell
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||||
++ lib.optionals cfg.enableVPN [
|
++ lib.optionals cfg.enableVPN [
|
||||||
|
|||||||
+2
-18
@@ -8,7 +8,6 @@
|
|||||||
let
|
let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
cfg = config.programs.dank-material-shell.greeter;
|
cfg = config.programs.dank-material-shell.greeter;
|
||||||
cfgDms = config.programs.dank-material-shell;
|
|
||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
@@ -30,13 +29,13 @@ let
|
|||||||
lib.escapeShellArgs (
|
lib.escapeShellArgs (
|
||||||
[
|
[
|
||||||
"sh"
|
"sh"
|
||||||
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
||||||
"--cache-dir"
|
"--cache-dir"
|
||||||
cacheDir
|
cacheDir
|
||||||
"--command"
|
"--command"
|
||||||
cfg.compositor.name
|
cfg.compositor.name
|
||||||
"-p"
|
"-p"
|
||||||
"${cfg.package}/share/quickshell/dms"
|
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||||
"-C"
|
"-C"
|
||||||
@@ -66,21 +65,6 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell.greeter = {
|
options.programs.dank-material-shell.greeter = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
||||||
package = lib.mkOption {
|
|
||||||
type = types.package;
|
|
||||||
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
|
|
||||||
defaultText = lib.literalExpression ''
|
|
||||||
if config.programs.dank-material-shell.enable
|
|
||||||
then config.programs.dank-material-shell.package
|
|
||||||
else built from source;
|
|
||||||
'';
|
|
||||||
description = ''
|
|
||||||
The DankMaterialShell package to use for the greeter.
|
|
||||||
|
|
||||||
Defaults to the package from `programs.dank-material-shell` if it is enabled,
|
|
||||||
otherwise defaults to building from source.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
compositor.name = lib.mkOption {
|
compositor.name = lib.mkOption {
|
||||||
type = types.enum [
|
type = types.enum [
|
||||||
"niri"
|
"niri"
|
||||||
|
|||||||
+3
-1
@@ -2,6 +2,7 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
|
dmsPkgs,
|
||||||
...
|
...
|
||||||
}@args:
|
}@args:
|
||||||
let
|
let
|
||||||
@@ -12,6 +13,7 @@ let
|
|||||||
config
|
config
|
||||||
pkgs
|
pkgs
|
||||||
lib
|
lib
|
||||||
|
dmsPkgs
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
||||||
@@ -94,7 +96,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe cfg.package + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
|
dmsPkgs,
|
||||||
...
|
...
|
||||||
}@args:
|
}@args:
|
||||||
let
|
let
|
||||||
@@ -11,6 +12,7 @@ let
|
|||||||
config
|
config
|
||||||
pkgs
|
pkgs
|
||||||
lib
|
lib
|
||||||
|
dmsPkgs
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -34,7 +36,7 @@ in
|
|||||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = lib.getExe cfg.package + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -48,7 +50,6 @@ in
|
|||||||
|
|
||||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
services.power-profiles-daemon.enable = lib.mkDefault true;
|
||||||
services.accounts-daemon.enable = lib.mkDefault true;
|
services.accounts-daemon.enable = lib.mkDefault true;
|
||||||
services.geoclue2.enable = lib.mkDefault true;
|
|
||||||
security.polkit.enable = lib.mkDefault true;
|
security.polkit.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell = {
|
options.programs.dank-material-shell = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell";
|
enable = lib.mkEnableOption "DankMaterialShell";
|
||||||
package = lib.mkPackageOption dmsPkgs "dms-shell" {
|
|
||||||
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||||
|
|||||||
@@ -56,10 +56,8 @@ mkdir -p $HOME $GOCACHE $GOMODCACHE
|
|||||||
# OBS has no network access, so use local toolchain only
|
# OBS has no network access, so use local toolchain only
|
||||||
export GOTOOLCHAIN=local
|
export GOTOOLCHAIN=local
|
||||||
|
|
||||||
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
|
# Patch go.mod to use base Go version (e.g., go 1.24 instead of go 1.24.6)
|
||||||
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
|
|
||||||
|
|
||||||
# Extract version info for embedding in binary
|
# Extract version info for embedding in binary
|
||||||
VERSION="%{version}"
|
VERSION="%{version}"
|
||||||
|
|||||||
@@ -102,19 +102,6 @@ if [[ ! -d "distro/debian" ]]; then
|
|||||||
echo "Error: Run this script from the repository root"
|
echo "Error: Run this script from the repository root"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
|
|
||||||
osc_retry() {
|
|
||||||
local max=3 attempt=1
|
|
||||||
while true; do
|
|
||||||
if osc "$@"; then return 0; fi
|
|
||||||
((attempt >= max)) && return 1
|
|
||||||
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
|
|
||||||
sleep $((5*attempt))
|
|
||||||
((attempt++))
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# $1 = PROJECT
|
# $1 = PROJECT
|
||||||
# $2 = PACKAGE
|
# $2 = PACKAGE
|
||||||
@@ -322,23 +309,8 @@ mkdir -p "$OBS_BASE"
|
|||||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||||
cd "$OBS_BASE"
|
cd "$OBS_BASE"
|
||||||
CHECKOUT_OK=false
|
osc co "$OBS_PROJECT/$PACKAGE"
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if osc co "$OBS_PROJECT/$PACKAGE"; then
|
|
||||||
CHECKOUT_OK=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ $attempt -lt 3 ]]; then
|
|
||||||
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
|
|
||||||
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
|
|
||||||
sleep $((5*attempt))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
|
||||||
echo "Error: Checkout failed after 3 attempts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
||||||
@@ -447,9 +419,6 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
|||||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
|
||||||
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
|
||||||
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
||||||
@@ -844,9 +813,6 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
|||||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||||
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
|
||||||
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
|
||||||
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1092,7 +1058,7 @@ fi
|
|||||||
|
|
||||||
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
||||||
echo "==> Updating working copy"
|
echo "==> Updating working copy"
|
||||||
if ! osc_retry up 2>/dev/null; then
|
if ! osc up 2>/dev/null; then
|
||||||
echo "Error: Failed to update working copy"
|
echo "Error: Failed to update working copy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1173,7 +1139,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
|||||||
else
|
else
|
||||||
echo "==> Committing to OBS"
|
echo "==> Committing to OBS"
|
||||||
set +e
|
set +e
|
||||||
osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
||||||
COMMIT_EXIT=${PIPESTATUS[0]}
|
COMMIT_EXIT=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Suggests: niri | hyprland | sway
|
Recommends: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -72,82 +72,76 @@
|
|||||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.lib.makeOverridable (
|
dms-shell = pkgs.buildGoModule (
|
||||||
|
let
|
||||||
|
rootSrc = ./.;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
extraQtPackages ? [ ],
|
inherit version;
|
||||||
}:
|
pname = "dms-shell";
|
||||||
pkgs.buildGoModule (
|
src = ./core;
|
||||||
let
|
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||||
rootSrc = ./.;
|
|
||||||
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit version;
|
|
||||||
pname = "dms-shell";
|
|
||||||
src = ./core;
|
|
||||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X 'main.Version=${version}'"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
mkdir -p $out/share/quickshell/dms
|
mkdir -p $out/share/quickshell/dms
|
||||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
chmod u+w $out/share/quickshell/dms/VERSION
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
|
|
||||||
# Install desktop file and icon
|
# Install desktop file and icon
|
||||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
$out/share/applications/dms-open.desktop
|
$out/share/applications/dms-open.desktop
|
||||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
wrapProgram $out/bin/dms \
|
wrapProgram $out/bin/dms \
|
||||||
--add-flags "-c $out/share/quickshell/dms" \
|
--add-flags "-c $out/share/quickshell/dms" \
|
||||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
||||||
|
|
||||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
$out/lib/systemd/user/dms.service
|
$out/lib/systemd/user/dms.service
|
||||||
|
|
||||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||||
|
|
||||||
installShellCompletion --cmd dms \
|
installShellCompletion --cmd dms \
|
||||||
--bash <($out/bin/dms completion bash) \
|
--bash <($out/bin/dms completion bash) \
|
||||||
--fish <($out/bin/dms completion fish) \
|
--fish <($out/bin/dms completion fish) \
|
||||||
--zsh <($out/bin/dms completion zsh)
|
--zsh <($out/bin/dms completion zsh)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||||
homepage = "https://danklinux.com";
|
homepage = "https://danklinux.com";
|
||||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||||
license = pkgs.lib.licenses.mit;
|
license = pkgs.lib.licenses.mit;
|
||||||
mainProgram = "dms";
|
mainProgram = "dms";
|
||||||
platforms = pkgs.lib.platforms.linux;
|
platforms = pkgs.lib.platforms.linux;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
) { };
|
|
||||||
|
|
||||||
quickshell = quickshell.packages.${system}.default;
|
quickshell = quickshell.packages.${system}.default;
|
||||||
|
|
||||||
@@ -188,7 +182,6 @@
|
|||||||
with pkgs;
|
with pkgs;
|
||||||
[
|
[
|
||||||
go_1_25
|
go_1_25
|
||||||
go-mockery_2
|
|
||||||
gopls
|
gopls
|
||||||
delve
|
delve
|
||||||
go-tools
|
go-tools
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
|
|||||||
|
|
||||||
# Code formatting and linting
|
# Code formatting and linting
|
||||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
|
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
|
||||||
make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .`
|
qmllint **/*.qml # Lint all QML files
|
||||||
./qmlformat-all.sh # Format all QML files
|
./qmlformat-all.sh # Format all QML files
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -783,7 +783,7 @@ When modifying the shell:
|
|||||||
|
|
||||||
**QML Frontend:**
|
**QML Frontend:**
|
||||||
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
||||||
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml`, then from repo root run `make lint-qml` after Quickshell has generated the local `.qmlls.ini` VFS with `qs -p .`
|
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml`
|
||||||
3. **Performance**: Ensure animations remain smooth (60 FPS target)
|
3. **Performance**: Ensure animations remain smooth (60 FPS target)
|
||||||
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
The Wolverine
|
Saffron Bloom
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var level: Theme.elevationLevel2
|
|
||||||
property string direction: Theme.elevationLightDirection
|
|
||||||
property real fallbackOffset: 4
|
|
||||||
|
|
||||||
property color targetColor: "white"
|
|
||||||
property real targetRadius: Theme.cornerRadius
|
|
||||||
property color borderColor: "transparent"
|
|
||||||
property real borderWidth: 0
|
|
||||||
|
|
||||||
property bool shadowEnabled: Theme.elevationEnabled
|
|
||||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
|
||||||
property real shadowSpreadPx: level && level.spreadPx !== undefined ? level.spreadPx : 0
|
|
||||||
property real shadowOffsetX: Theme.elevationOffsetXFor(level, direction, fallbackOffset)
|
|
||||||
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
|
|
||||||
property color shadowColor: Theme.elevationShadowColor(level)
|
|
||||||
property real shadowOpacity: 1
|
|
||||||
property real blurMax: Theme.elevationBlurMax
|
|
||||||
|
|
||||||
property alias sourceRect: sourceRect
|
|
||||||
|
|
||||||
layer.enabled: shadowEnabled
|
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
autoPaddingEnabled: true
|
|
||||||
shadowEnabled: true
|
|
||||||
blurEnabled: false
|
|
||||||
maskEnabled: false
|
|
||||||
shadowBlur: Math.max(0, Math.min(1, root.shadowBlurPx / Math.max(1, root.blurMax)))
|
|
||||||
shadowScale: 1 + (2 * root.shadowSpreadPx) / Math.max(1, Math.min(root.width, root.height))
|
|
||||||
shadowHorizontalOffset: root.shadowOffsetX
|
|
||||||
shadowVerticalOffset: root.shadowOffsetY
|
|
||||||
blurMax: root.blurMax
|
|
||||||
shadowColor: root.shadowColor
|
|
||||||
shadowOpacity: root.shadowOpacity
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: sourceRect
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: root.targetRadius
|
|
||||||
color: root.targetColor
|
|
||||||
border.color: root.borderColor
|
|
||||||
border.width: root.borderWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
-37
@@ -1,6 +1,5 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.folderlistmodel
|
import Qt.labs.folderlistmodel
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -9,9 +8,7 @@ import Quickshell.Io
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string _resolvedLocale: "en"
|
readonly property string _rawLocale: Qt.locale().name
|
||||||
|
|
||||||
readonly property string _rawLocale: SessionData.locale === "" ? Qt.locale().name : SessionData.locale
|
|
||||||
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
||||||
readonly property var _candidates: {
|
readonly property var _candidates: {
|
||||||
const fullUnderscore = _rawLocale;
|
const fullUnderscore = _rawLocale;
|
||||||
@@ -24,10 +21,7 @@ Singleton {
|
|||||||
|
|
||||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
||||||
|
|
||||||
readonly property alias folder: dir.folder
|
property string currentLocale: "en"
|
||||||
property var presentLocales: ({
|
|
||||||
"en": Qt.locale("en")
|
|
||||||
})
|
|
||||||
property var translations: ({})
|
property var translations: ({})
|
||||||
property bool translationsLoaded: false
|
property bool translationsLoaded: false
|
||||||
|
|
||||||
@@ -40,10 +34,8 @@ Singleton {
|
|||||||
showDirs: false
|
showDirs: false
|
||||||
showDotAndDotDot: false
|
showDotAndDotDot: false
|
||||||
|
|
||||||
onStatusChanged: if (status === FolderListModel.Ready) {
|
onStatusChanged: if (status === FolderListModel.Ready)
|
||||||
root._loadPresentLocales();
|
root._pickTranslation()
|
||||||
root._pickTranslation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -54,54 +46,41 @@ Singleton {
|
|||||||
try {
|
try {
|
||||||
root.translations = JSON.parse(text());
|
root.translations = JSON.parse(text());
|
||||||
root.translationsLoaded = true;
|
root.translationsLoaded = true;
|
||||||
console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
|
console.info(`I18n: Loaded translations for '${root.currentLocale}' ` + `(${Object.keys(root.translations).length} contexts)`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
|
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e, "- falling back to English");
|
||||||
root._fallbackToEnglish();
|
root._fallbackToEnglish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
|
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` + "falling back to English");
|
||||||
root._fallbackToEnglish();
|
root._fallbackToEnglish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function locale() {
|
function _pickTranslation() {
|
||||||
if (SessionData.timeLocale)
|
const present = new Set();
|
||||||
return Qt.locale(SessionData.timeLocale);
|
|
||||||
return Qt.locale();
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loadPresentLocales() {
|
|
||||||
if (Object.keys(presentLocales).length > 1) {
|
|
||||||
return; // already loaded
|
|
||||||
}
|
|
||||||
for (let i = 0; i < dir.count; i++) {
|
for (let i = 0; i < dir.count; i++) {
|
||||||
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
|
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
|
||||||
if (name && name.endsWith(".json")) {
|
if (name && name.endsWith(".json")) {
|
||||||
const shortName = name.slice(0, -5);
|
present.add(name.slice(0, -5));
|
||||||
presentLocales[shortName] = Qt.locale(shortName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function _pickTranslation() {
|
|
||||||
for (let i = 0; i < _candidates.length; i++) {
|
for (let i = 0; i < _candidates.length; i++) {
|
||||||
const cand = _candidates[i];
|
const cand = _candidates[i];
|
||||||
if (presentLocales[cand] === undefined)
|
if (present.has(cand)) {
|
||||||
continue;
|
_useLocale(cand, dir.folder + "/" + cand + ".json");
|
||||||
_resolvedLocale = cand;
|
return;
|
||||||
useLocale(cand, cand.startsWith("en") ? "" : translationsFolder + "/" + cand + ".json");
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolvedLocale = "en";
|
|
||||||
_fallbackToEnglish();
|
_fallbackToEnglish();
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLocale(localeTag, fileUrl) {
|
function _useLocale(localeTag, fileUrl) {
|
||||||
_resolvedLocale = localeTag || "en";
|
currentLocale = localeTag;
|
||||||
_selectedPath = fileUrl;
|
_selectedPath = fileUrl;
|
||||||
translationsLoaded = false;
|
translationsLoaded = false;
|
||||||
translations = ({});
|
translations = ({});
|
||||||
@@ -109,6 +88,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _fallbackToEnglish() {
|
function _fallbackToEnglish() {
|
||||||
|
currentLocale = "en";
|
||||||
_selectedPath = "";
|
_selectedPath = "";
|
||||||
translationsLoaded = false;
|
translationsLoaded = false;
|
||||||
translations = ({});
|
translations = ({});
|
||||||
|
|||||||
@@ -71,40 +71,15 @@ Singleton {
|
|||||||
return appId;
|
return appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveIconPath(iconName: string): string {
|
|
||||||
if (!iconName) return "";
|
|
||||||
const moddedId = moddedAppId(iconName);
|
|
||||||
if (moddedId !== iconName) {
|
|
||||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
|
||||||
return toFileUrl(expandTilde(moddedId));
|
|
||||||
if (moddedId.startsWith("file://"))
|
|
||||||
return moddedId;
|
|
||||||
return Quickshell.iconPath(moddedId, true);
|
|
||||||
}
|
|
||||||
return Quickshell.iconPath(iconName, true) || DesktopService.resolveIconPath(iconName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveIconUrl(iconName: string): string {
|
|
||||||
if (!iconName) return "";
|
|
||||||
const moddedId = moddedAppId(iconName);
|
|
||||||
if (moddedId !== iconName) {
|
|
||||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
|
||||||
return toFileUrl(expandTilde(moddedId));
|
|
||||||
if (moddedId.startsWith("file://"))
|
|
||||||
return moddedId;
|
|
||||||
return "image://icon/" + moddedId;
|
|
||||||
}
|
|
||||||
return "image://icon/" + iconName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||||
if (appId === "org.quickshell") {
|
if (appId === "org.quickshell") {
|
||||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
const moddedId = moddedAppId(appId);
|
const moddedId = moddedAppId(appId);
|
||||||
if (moddedId !== appId)
|
if (moddedId !== appId) {
|
||||||
return resolveIconPath(appId);
|
return Quickshell.iconPath(moddedId, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (desktopEntry && desktopEntry.icon) {
|
if (desktopEntry && desktopEntry.icon) {
|
||||||
return Quickshell.iconPath(desktopEntry.icon, true);
|
return Quickshell.iconPath(desktopEntry.icon, true);
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ Singleton {
|
|||||||
property bool _isReadOnly: false
|
property bool _isReadOnly: false
|
||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
property var _loadedSessionSnapshot: null
|
property var _loadedSessionSnapshot: null
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({})
|
||||||
"updateLocale": updateLocale
|
|
||||||
})
|
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -128,9 +126,6 @@ Singleton {
|
|||||||
property var hiddenOutputDeviceNames: []
|
property var hiddenOutputDeviceNames: []
|
||||||
property var hiddenInputDeviceNames: []
|
property var hiddenInputDeviceNames: []
|
||||||
|
|
||||||
property string locale: ""
|
|
||||||
property string timeLocale: ""
|
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
property string appDrawerLastMode: "apps"
|
property string appDrawerLastMode: "apps"
|
||||||
property string niriOverviewLastMode: "apps"
|
property string niriOverviewLastMode: "apps"
|
||||||
@@ -1109,14 +1104,6 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLocale() {
|
|
||||||
if (!locale) {
|
|
||||||
I18n._pickTranslation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLauncherLastMode(mode) {
|
function setLauncherLastMode(mode) {
|
||||||
launcherLastMode = mode;
|
launcherLastMode = mode;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -1245,7 +1232,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
||||||
return greetCfgDir + "/session.json";
|
return greetCfgDir + "/session.json";
|
||||||
}
|
}
|
||||||
preload: isGreeterMode
|
preload: isGreeterMode
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int settingsConfigVersion: 6
|
readonly property int settingsConfigVersion: 5
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||||
|
|
||||||
@@ -149,7 +149,6 @@ Singleton {
|
|||||||
property int mangoLayoutRadiusOverride: -1
|
property int mangoLayoutRadiusOverride: -1
|
||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
|
|
||||||
property int firstDayOfWeek: -1
|
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
@@ -166,24 +165,6 @@ Singleton {
|
|||||||
property int modalCustomAnimationDuration: 150
|
property int modalCustomAnimationDuration: 150
|
||||||
property bool enableRippleEffects: true
|
property bool enableRippleEffects: true
|
||||||
onEnableRippleEffectsChanged: saveSettings()
|
onEnableRippleEffectsChanged: saveSettings()
|
||||||
property bool m3ElevationEnabled: true
|
|
||||||
onM3ElevationEnabledChanged: saveSettings()
|
|
||||||
property int m3ElevationIntensity: 12
|
|
||||||
onM3ElevationIntensityChanged: saveSettings()
|
|
||||||
property int m3ElevationOpacity: 30
|
|
||||||
onM3ElevationOpacityChanged: saveSettings()
|
|
||||||
property string m3ElevationColorMode: "default"
|
|
||||||
onM3ElevationColorModeChanged: saveSettings()
|
|
||||||
property string m3ElevationLightDirection: "top"
|
|
||||||
onM3ElevationLightDirectionChanged: saveSettings()
|
|
||||||
property string m3ElevationCustomColor: "#000000"
|
|
||||||
onM3ElevationCustomColorChanged: saveSettings()
|
|
||||||
property bool modalElevationEnabled: true
|
|
||||||
onModalElevationEnabledChanged: saveSettings()
|
|
||||||
property bool popoutElevationEnabled: true
|
|
||||||
onPopoutElevationEnabledChanged: saveSettings()
|
|
||||||
property bool barElevationEnabled: true
|
|
||||||
onBarElevationEnabledChanged: saveSettings()
|
|
||||||
property string wallpaperFillMode: "Fill"
|
property string wallpaperFillMode: "Fill"
|
||||||
property bool blurredWallpaperLayer: false
|
property bool blurredWallpaperLayer: false
|
||||||
property bool blurWallpaperOnOverview: false
|
property bool blurWallpaperOnOverview: false
|
||||||
@@ -280,7 +261,6 @@ Singleton {
|
|||||||
property bool showOccupiedWorkspacesOnly: false
|
property bool showOccupiedWorkspacesOnly: false
|
||||||
property bool reverseScrolling: false
|
property bool reverseScrolling: false
|
||||||
property bool dwlShowAllTags: false
|
property bool dwlShowAllTags: false
|
||||||
property bool workspaceActiveAppHighlightEnabled: false
|
|
||||||
property string workspaceColorMode: "default"
|
property string workspaceColorMode: "default"
|
||||||
property string workspaceOccupiedColorMode: "none"
|
property string workspaceOccupiedColorMode: "none"
|
||||||
property string workspaceUnfocusedColorMode: "default"
|
property string workspaceUnfocusedColorMode: "default"
|
||||||
@@ -314,17 +294,6 @@ Singleton {
|
|||||||
property string centeringMode: "index"
|
property string centeringMode: "index"
|
||||||
property string clockDateFormat: ""
|
property string clockDateFormat: ""
|
||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool greeterRememberLastSession: true
|
|
||||||
property bool greeterRememberLastUser: true
|
|
||||||
property bool greeterEnableFprint: false
|
|
||||||
property bool greeterEnableU2f: false
|
|
||||||
property string greeterWallpaperPath: ""
|
|
||||||
property bool greeterUse24HourClock: true
|
|
||||||
property bool greeterShowSeconds: false
|
|
||||||
property bool greeterPadHours12Hour: false
|
|
||||||
property string greeterLockDateFormat: ""
|
|
||||||
property string greeterFontFamily: ""
|
|
||||||
property string greeterWallpaperFillMode: ""
|
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -468,20 +437,18 @@ Singleton {
|
|||||||
property bool matugenTemplateGhostty: true
|
property bool matugenTemplateGhostty: true
|
||||||
property bool matugenTemplateKitty: true
|
property bool matugenTemplateKitty: true
|
||||||
property bool matugenTemplateFoot: true
|
property bool matugenTemplateFoot: true
|
||||||
property bool matugenTemplateNeovim: false
|
property bool matugenTemplateNeovim: true
|
||||||
property bool matugenTemplateAlacritty: true
|
property bool matugenTemplateAlacritty: true
|
||||||
property bool matugenTemplateWezterm: true
|
property bool matugenTemplateWezterm: true
|
||||||
property bool matugenTemplateDgop: true
|
property bool matugenTemplateDgop: true
|
||||||
property bool matugenTemplateKcolorscheme: true
|
property bool matugenTemplateKcolorscheme: true
|
||||||
property bool matugenTemplateVscode: true
|
property bool matugenTemplateVscode: true
|
||||||
property bool matugenTemplateEmacs: true
|
property bool matugenTemplateEmacs: true
|
||||||
property bool matugenTemplateZed: true
|
|
||||||
|
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
property bool dockSmartAutoHide: false
|
property bool dockSmartAutoHide: false
|
||||||
property bool dockGroupByApp: false
|
property bool dockGroupByApp: false
|
||||||
property bool dockRestoreSpecialWorkspaceOnClick: false
|
|
||||||
property bool dockOpenOnOverview: false
|
property bool dockOpenOnOverview: false
|
||||||
property int dockPosition: SettingsData.Position.Bottom
|
property int dockPosition: SettingsData.Position.Bottom
|
||||||
property real dockSpacing: 4
|
property real dockSpacing: 4
|
||||||
@@ -527,29 +494,9 @@ Singleton {
|
|||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
property bool lockFingerprintCanEnable: false
|
|
||||||
property bool lockFingerprintReady: false
|
|
||||||
property string lockFingerprintReason: "probe_failed"
|
|
||||||
property bool greeterFingerprintCanEnable: false
|
|
||||||
property bool greeterFingerprintReady: false
|
|
||||||
property string greeterFingerprintReason: "probe_failed"
|
|
||||||
property string greeterFingerprintSource: "none"
|
|
||||||
property bool enableU2f: false
|
|
||||||
property string u2fMode: "or"
|
|
||||||
property bool u2fAvailable: false
|
|
||||||
property bool lockU2fCanEnable: false
|
|
||||||
property bool lockU2fReady: false
|
|
||||||
property string lockU2fReason: "probe_failed"
|
|
||||||
property bool greeterU2fCanEnable: false
|
|
||||||
property bool greeterU2fReady: false
|
|
||||||
property string greeterU2fReason: "probe_failed"
|
|
||||||
property string greeterU2fSource: "none"
|
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
property bool lockScreenVideoEnabled: false
|
|
||||||
property string lockScreenVideoPath: ""
|
|
||||||
property bool lockScreenVideoCycling: false
|
|
||||||
property bool hideBrightnessSlider: false
|
property bool hideBrightnessSlider: false
|
||||||
|
|
||||||
property int notificationTimeoutLow: 5000
|
property int notificationTimeoutLow: 5000
|
||||||
@@ -566,7 +513,6 @@ Singleton {
|
|||||||
property bool notificationHistorySaveNormal: true
|
property bool notificationHistorySaveNormal: true
|
||||||
property bool notificationHistorySaveCritical: true
|
property bool notificationHistorySaveCritical: true
|
||||||
property var notificationRules: []
|
property var notificationRules: []
|
||||||
property bool notificationFocusedMonitor: false
|
|
||||||
|
|
||||||
property bool osdAlwaysShowValue: false
|
property bool osdAlwaysShowValue: false
|
||||||
property int osdPosition: SettingsData.Position.BottomCenter
|
property int osdPosition: SettingsData.Position.BottomCenter
|
||||||
@@ -656,7 +602,7 @@ Singleton {
|
|||||||
"scrollYBehavior": "workspace",
|
"scrollYBehavior": "workspace",
|
||||||
"shadowIntensity": 0,
|
"shadowIntensity": 0,
|
||||||
"shadowOpacity": 60,
|
"shadowOpacity": 60,
|
||||||
"shadowColorMode": "default",
|
"shadowColorMode": "text",
|
||||||
"shadowCustomColor": "#000000",
|
"shadowCustomColor": "#000000",
|
||||||
"clickThrough": false
|
"clickThrough": false
|
||||||
}
|
}
|
||||||
@@ -1030,19 +976,12 @@ Singleton {
|
|||||||
signal widgetDataChanged
|
signal widgetDataChanged
|
||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
function refreshAuthAvailability() {
|
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Processes.settingsRoot = root;
|
|
||||||
Processes.detectAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
refreshAuthAvailability();
|
Processes.detectFprintd();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1282,47 +1221,10 @@ Singleton {
|
|||||||
return JSON.stringify(Store.toJson(root), null, 2);
|
return JSON.stringify(Store.toJson(root), null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resetPluginSettings() {
|
|
||||||
_pluginParseError = false;
|
|
||||||
pluginSettings = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pluginSettingsErrorCode(error) {
|
|
||||||
if (typeof error === "number")
|
|
||||||
return error;
|
|
||||||
if (error && typeof error === "object") {
|
|
||||||
if (typeof error.code === "number")
|
|
||||||
return error.code;
|
|
||||||
if (typeof error.errno === "number")
|
|
||||||
return error.errno;
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = String(error || "").trim();
|
|
||||||
if (/^\d+$/.test(msg))
|
|
||||||
return Number(msg);
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _isMissingPluginSettingsError(error) {
|
|
||||||
if (_pluginSettingsErrorCode(error) === 2)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const msg = String(error || "").toLowerCase();
|
|
||||||
return msg.indexOf("file does not exist") !== -1
|
|
||||||
|| msg.indexOf("no such file") !== -1
|
|
||||||
|| msg.indexOf("enoent") !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPluginSettings() {
|
function loadPluginSettings() {
|
||||||
try {
|
_pluginSettingsLoading = true;
|
||||||
parsePluginSettings(pluginSettingsFile.text());
|
parsePluginSettings(pluginSettingsFile.text());
|
||||||
} catch (e) {
|
_pluginSettingsLoading = false;
|
||||||
const msg = e.message || String(e);
|
|
||||||
if (!_isMissingPluginSettingsError(e))
|
|
||||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
|
||||||
_resetPluginSettings();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePluginSettings(content) {
|
function parsePluginSettings(content) {
|
||||||
@@ -2758,7 +2660,6 @@ Singleton {
|
|||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
printErrors: false
|
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: !isGreeterMode
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
@@ -2767,10 +2668,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
const msg = String(error || "");
|
pluginSettings = {};
|
||||||
if (!_isMissingPluginSettingsError(error))
|
|
||||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
|
||||||
_resetPluginSettings();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-231
@@ -673,232 +673,6 @@ Singleton {
|
|||||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
|
||||||
readonly property bool elevationEnabled: typeof SettingsData !== "undefined" && (SettingsData.m3ElevationEnabled ?? true)
|
|
||||||
readonly property real elevationBlurMax: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? Math.min(128, Math.max(32, SettingsData.m3ElevationIntensity * 2)) : 64
|
|
||||||
|
|
||||||
readonly property real _elevMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? SettingsData.m3ElevationIntensity / 12 : 1
|
|
||||||
readonly property real _opMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationOpacity !== undefined ? SettingsData.m3ElevationOpacity / 60 : 1
|
|
||||||
function normalizeElevationDirection(direction) {
|
|
||||||
switch (direction) {
|
|
||||||
case "top":
|
|
||||||
case "topLeft":
|
|
||||||
case "topRight":
|
|
||||||
case "bottom":
|
|
||||||
case "bottomLeft":
|
|
||||||
case "bottomRight":
|
|
||||||
case "left":
|
|
||||||
case "right":
|
|
||||||
case "autoBar":
|
|
||||||
return direction;
|
|
||||||
default:
|
|
||||||
return "top";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property string elevationLightDirection: {
|
|
||||||
if (typeof SettingsData === "undefined" || !SettingsData.m3ElevationLightDirection)
|
|
||||||
return "top";
|
|
||||||
switch (SettingsData.m3ElevationLightDirection) {
|
|
||||||
case "autoBar":
|
|
||||||
case "top":
|
|
||||||
case "topLeft":
|
|
||||||
case "topRight":
|
|
||||||
case "bottom":
|
|
||||||
return SettingsData.m3ElevationLightDirection;
|
|
||||||
default:
|
|
||||||
return "top";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property real _elevDiagRatio: 0.55
|
|
||||||
readonly property string _globalElevationDirForTokens: {
|
|
||||||
const normalized = normalizeElevationDirection(elevationLightDirection);
|
|
||||||
return normalized === "autoBar" ? "top" : normalized;
|
|
||||||
}
|
|
||||||
readonly property real _elevDirX: {
|
|
||||||
switch (_globalElevationDirForTokens) {
|
|
||||||
case "topLeft":
|
|
||||||
case "bottomLeft":
|
|
||||||
case "left":
|
|
||||||
return 1;
|
|
||||||
case "topRight":
|
|
||||||
case "bottomRight":
|
|
||||||
case "right":
|
|
||||||
return -1;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property real _elevDirY: {
|
|
||||||
switch (_globalElevationDirForTokens) {
|
|
||||||
case "bottom":
|
|
||||||
case "bottomLeft":
|
|
||||||
case "bottomRight":
|
|
||||||
return -1;
|
|
||||||
case "left":
|
|
||||||
case "right":
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property real _elevDirXScale: (_globalElevationDirForTokens === "left" || _globalElevationDirForTokens === "right") ? 1 : _elevDiagRatio
|
|
||||||
|
|
||||||
readonly property var elevationLevel1: ({
|
|
||||||
blurPx: 4 * _elevMult,
|
|
||||||
offsetX: 1 * _elevMult * _elevDirXScale * _elevDirX,
|
|
||||||
offsetY: 1 * _elevMult * _elevDirY,
|
|
||||||
spreadPx: 0,
|
|
||||||
alpha: 0.2 * _opMult
|
|
||||||
})
|
|
||||||
readonly property var elevationLevel2: ({
|
|
||||||
blurPx: 8 * _elevMult,
|
|
||||||
offsetX: 4 * _elevMult * _elevDirXScale * _elevDirX,
|
|
||||||
offsetY: 4 * _elevMult * _elevDirY,
|
|
||||||
spreadPx: 0,
|
|
||||||
alpha: 0.25 * _opMult
|
|
||||||
})
|
|
||||||
readonly property var elevationLevel3: ({
|
|
||||||
blurPx: 12 * _elevMult,
|
|
||||||
offsetX: 6 * _elevMult * _elevDirXScale * _elevDirX,
|
|
||||||
offsetY: 6 * _elevMult * _elevDirY,
|
|
||||||
spreadPx: 0,
|
|
||||||
alpha: 0.3 * _opMult
|
|
||||||
})
|
|
||||||
readonly property var elevationLevel4: ({
|
|
||||||
blurPx: 16 * _elevMult,
|
|
||||||
offsetX: 8 * _elevMult * _elevDirXScale * _elevDirX,
|
|
||||||
offsetY: 8 * _elevMult * _elevDirY,
|
|
||||||
spreadPx: 0,
|
|
||||||
alpha: 0.3 * _opMult
|
|
||||||
})
|
|
||||||
readonly property var elevationLevel5: ({
|
|
||||||
blurPx: 20 * _elevMult,
|
|
||||||
offsetX: 10 * _elevMult * _elevDirXScale * _elevDirX,
|
|
||||||
offsetY: 10 * _elevMult * _elevDirY,
|
|
||||||
spreadPx: 0,
|
|
||||||
alpha: 0.3 * _opMult
|
|
||||||
})
|
|
||||||
|
|
||||||
function elevationOffsetMagnitude(level, fallback, direction) {
|
|
||||||
if (!level) {
|
|
||||||
return fallback !== undefined ? Math.abs(fallback) : 0;
|
|
||||||
}
|
|
||||||
const yMag = Math.abs(level.offsetY !== undefined ? level.offsetY : 0);
|
|
||||||
if (yMag > 0)
|
|
||||||
return yMag;
|
|
||||||
const xMag = Math.abs(level.offsetX !== undefined ? level.offsetX : 0);
|
|
||||||
if (xMag > 0) {
|
|
||||||
if (direction === "left" || direction === "right")
|
|
||||||
return xMag;
|
|
||||||
return xMag / _elevDiagRatio;
|
|
||||||
}
|
|
||||||
return fallback !== undefined ? Math.abs(fallback) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function elevationOffsetXFor(level, direction, fallback) {
|
|
||||||
const dir = normalizeElevationDirection(direction || elevationLightDirection);
|
|
||||||
const mag = elevationOffsetMagnitude(level, fallback, dir);
|
|
||||||
switch (dir) {
|
|
||||||
case "topLeft":
|
|
||||||
case "bottomLeft":
|
|
||||||
return mag * _elevDiagRatio;
|
|
||||||
case "topRight":
|
|
||||||
case "bottomRight":
|
|
||||||
return -mag * _elevDiagRatio;
|
|
||||||
case "left":
|
|
||||||
return mag;
|
|
||||||
case "right":
|
|
||||||
return -mag;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function elevationOffsetYFor(level, direction, fallback) {
|
|
||||||
const dir = normalizeElevationDirection(direction || elevationLightDirection);
|
|
||||||
const mag = elevationOffsetMagnitude(level, fallback, dir);
|
|
||||||
switch (dir) {
|
|
||||||
case "bottom":
|
|
||||||
case "bottomLeft":
|
|
||||||
case "bottomRight":
|
|
||||||
return -mag;
|
|
||||||
case "left":
|
|
||||||
case "right":
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
return mag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function elevationOffsetX(level, fallback) {
|
|
||||||
return elevationOffsetXFor(level, elevationLightDirection, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function elevationOffsetY(level, fallback) {
|
|
||||||
return elevationOffsetYFor(level, elevationLightDirection, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function elevationRenderPadding(level, direction, fallbackOffset, extraPadding, minPadding) {
|
|
||||||
const dir = direction !== undefined ? direction : elevationLightDirection;
|
|
||||||
const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
|
|
||||||
const spread = (level && level.spreadPx !== undefined) ? Math.max(0, level.spreadPx) : 0;
|
|
||||||
const fallback = fallbackOffset !== undefined ? fallbackOffset : 0;
|
|
||||||
const extra = extraPadding !== undefined ? extraPadding : 8;
|
|
||||||
const minPad = minPadding !== undefined ? minPadding : 16;
|
|
||||||
const offsetX = Math.abs(elevationOffsetXFor(level, dir, fallback));
|
|
||||||
const offsetY = Math.abs(elevationOffsetYFor(level, dir, fallback));
|
|
||||||
return Math.max(minPad, blur + spread + Math.max(offsetX, offsetY) + extra);
|
|
||||||
}
|
|
||||||
|
|
||||||
function elevationShadowColor(level) {
|
|
||||||
const alpha = (level && level.alpha !== undefined) ? level.alpha : 0.3;
|
|
||||||
let r = 0;
|
|
||||||
let g = 0;
|
|
||||||
let b = 0;
|
|
||||||
|
|
||||||
if (typeof SettingsData !== "undefined") {
|
|
||||||
const mode = SettingsData.m3ElevationColorMode || "default";
|
|
||||||
if (mode === "default") {
|
|
||||||
r = 0;
|
|
||||||
g = 0;
|
|
||||||
b = 0;
|
|
||||||
} else if (mode === "text") {
|
|
||||||
r = surfaceText.r;
|
|
||||||
g = surfaceText.g;
|
|
||||||
b = surfaceText.b;
|
|
||||||
} else if (mode === "primary") {
|
|
||||||
r = primary.r;
|
|
||||||
g = primary.g;
|
|
||||||
b = primary.b;
|
|
||||||
} else if (mode === "surfaceVariant") {
|
|
||||||
r = surfaceVariant.r;
|
|
||||||
g = surfaceVariant.g;
|
|
||||||
b = surfaceVariant.b;
|
|
||||||
} else if (mode === "custom" && SettingsData.m3ElevationCustomColor) {
|
|
||||||
const c = Qt.color(SettingsData.m3ElevationCustomColor);
|
|
||||||
r = c.r;
|
|
||||||
g = c.g;
|
|
||||||
b = c.b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Qt.rgba(r, g, b, alpha);
|
|
||||||
}
|
|
||||||
function elevationTintOpacity(level) {
|
|
||||||
if (!level)
|
|
||||||
return 0;
|
|
||||||
if (level === elevationLevel1)
|
|
||||||
return 0.05;
|
|
||||||
if (level === elevationLevel2)
|
|
||||||
return 0.08;
|
|
||||||
if (level === elevationLevel3)
|
|
||||||
return 0.11;
|
|
||||||
if (level === elevationLevel4)
|
|
||||||
return 0.12;
|
|
||||||
if (level === elevationLevel5)
|
|
||||||
return 0.14;
|
|
||||||
return 0.08;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var animationDurations: [
|
readonly property var animationDurations: [
|
||||||
{
|
{
|
||||||
"shorter": 0,
|
"shorter": 0,
|
||||||
@@ -1084,7 +858,7 @@ Singleton {
|
|||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: {
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||||
return GreetdSettings.getEffectiveFontFamily();
|
return GreetdSettings.fontFamily;
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -1551,7 +1325,7 @@ Singleton {
|
|||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
const skipTemplates = [];
|
const skipTemplates = [];
|
||||||
if (!SettingsData.runDmsMatugenTemplates) {
|
if (!SettingsData.runDmsMatugenTemplates) {
|
||||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
|
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
|
||||||
} else {
|
} else {
|
||||||
if (!SettingsData.matugenTemplateGtk)
|
if (!SettingsData.matugenTemplateGtk)
|
||||||
skipTemplates.push("gtk");
|
skipTemplates.push("gtk");
|
||||||
@@ -1595,8 +1369,6 @@ Singleton {
|
|||||||
skipTemplates.push("vscode");
|
skipTemplates.push("vscode");
|
||||||
if (!SettingsData.matugenTemplateEmacs)
|
if (!SettingsData.matugenTemplateEmacs)
|
||||||
skipTemplates.push("emacs");
|
skipTemplates.push("emacs");
|
||||||
if (!SettingsData.matugenTemplateZed)
|
|
||||||
skipTemplates.push("zed");
|
|
||||||
}
|
}
|
||||||
if (skipTemplates.length > 0) {
|
if (skipTemplates.length > 0) {
|
||||||
args.push("--skip-templates", skipTemplates.join(","));
|
args.push("--skip-templates", skipTemplates.join(","));
|
||||||
@@ -1989,7 +1761,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,352 +10,18 @@ Singleton {
|
|||||||
|
|
||||||
property var settingsRoot: null
|
property var settingsRoot: null
|
||||||
|
|
||||||
property string greetdPamText: ""
|
|
||||||
property string systemAuthPamText: ""
|
|
||||||
property string commonAuthPamText: ""
|
|
||||||
property string passwordAuthPamText: ""
|
|
||||||
property string systemLoginPamText: ""
|
|
||||||
property string systemLocalLoginPamText: ""
|
|
||||||
property string commonAuthPcPamText: ""
|
|
||||||
property string loginPamText: ""
|
|
||||||
property string dankshellU2fPamText: ""
|
|
||||||
property string u2fKeysText: ""
|
|
||||||
|
|
||||||
property string fingerprintProbeOutput: ""
|
|
||||||
property int fingerprintProbeExitCode: 0
|
|
||||||
property bool fingerprintProbeStreamFinished: false
|
|
||||||
property bool fingerprintProbeExited: false
|
|
||||||
property string fingerprintProbeState: "probe_failed"
|
|
||||||
|
|
||||||
property string pamSupportProbeOutput: ""
|
|
||||||
property bool pamSupportProbeStreamFinished: false
|
|
||||||
property bool pamSupportProbeExited: false
|
|
||||||
property int pamSupportProbeExitCode: 0
|
|
||||||
property bool pamFprintSupportDetected: false
|
|
||||||
property bool pamU2fSupportDetected: false
|
|
||||||
|
|
||||||
readonly property string homeDir: Quickshell.env("HOME") || ""
|
|
||||||
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
|
|
||||||
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
|
|
||||||
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
|
|
||||||
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
|
|
||||||
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
|
||||||
|
|
||||||
function envFlag(name) {
|
|
||||||
const value = (Quickshell.env(name) || "").trim().toLowerCase();
|
|
||||||
if (value === "1" || value === "true" || value === "yes" || value === "on")
|
|
||||||
return true;
|
|
||||||
if (value === "0" || value === "false" || value === "no" || value === "off")
|
|
||||||
return false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
|
||||||
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
|
||||||
|
|
||||||
function detectQtTools() {
|
function detectQtTools() {
|
||||||
qtToolsDetectionProcess.running = true;
|
qtToolsDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectAuthCapabilities() {
|
|
||||||
if (!settingsRoot)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (forcedFprintAvailable === null) {
|
|
||||||
fingerprintProbeOutput = "";
|
|
||||||
fingerprintProbeStreamFinished = false;
|
|
||||||
fingerprintProbeExited = false;
|
|
||||||
fingerprintProbeProcess.running = true;
|
|
||||||
} else {
|
|
||||||
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
|
|
||||||
pamFprintSupportDetected = false;
|
|
||||||
pamU2fSupportDetected = false;
|
|
||||||
pamSupportProbeOutput = "";
|
|
||||||
pamSupportProbeStreamFinished = false;
|
|
||||||
pamSupportProbeExited = false;
|
|
||||||
pamSupportDetectionProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectFprintd() {
|
function detectFprintd() {
|
||||||
detectAuthCapabilities();
|
fprintdDetectionProcess.running = true;
|
||||||
}
|
|
||||||
|
|
||||||
function detectU2f() {
|
|
||||||
detectAuthCapabilities();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
function checkPluginSettings() {
|
||||||
pluginSettingsCheckProcess.running = true;
|
pluginSettingsCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripPamComment(line) {
|
|
||||||
if (!line)
|
|
||||||
return "";
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (!trimmed || trimmed.startsWith("#"))
|
|
||||||
return "";
|
|
||||||
const hashIdx = trimmed.indexOf("#");
|
|
||||||
if (hashIdx >= 0)
|
|
||||||
return trimmed.substring(0, hashIdx).trim();
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pamModuleEnabled(pamText, moduleName) {
|
|
||||||
if (!pamText || !moduleName)
|
|
||||||
return false;
|
|
||||||
const lines = pamText.split(/\r?\n/);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = stripPamComment(lines[i]);
|
|
||||||
if (!line)
|
|
||||||
continue;
|
|
||||||
if (line.includes(moduleName))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pamTextIncludesFile(pamText, filename) {
|
|
||||||
if (!pamText || !filename)
|
|
||||||
return false;
|
|
||||||
const lines = pamText.split(/\r?\n/);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = stripPamComment(lines[i]);
|
|
||||||
if (!line)
|
|
||||||
continue;
|
|
||||||
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function greeterPamStackHasModule(moduleName) {
|
|
||||||
if (pamModuleEnabled(greetdPamText, moduleName))
|
|
||||||
return true;
|
|
||||||
const includedPamStacks = [
|
|
||||||
["system-auth", systemAuthPamText],
|
|
||||||
["common-auth", commonAuthPamText],
|
|
||||||
["password-auth", passwordAuthPamText],
|
|
||||||
["system-login", systemLoginPamText],
|
|
||||||
["system-local-login", systemLocalLoginPamText],
|
|
||||||
["common-auth-pc", commonAuthPcPamText],
|
|
||||||
["login", loginPamText]
|
|
||||||
];
|
|
||||||
for (let i = 0; i < includedPamStacks.length; i++) {
|
|
||||||
const stack = includedPamStacks[i];
|
|
||||||
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasEnrolledFingerprintOutput(output) {
|
|
||||||
const lower = (output || "").toLowerCase();
|
|
||||||
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
|
|
||||||
return true;
|
|
||||||
const lines = lower.split(/\r?\n/);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const trimmed = lines[i].trim();
|
|
||||||
if (trimmed.startsWith("finger:"))
|
|
||||||
return true;
|
|
||||||
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasMissingFingerprintEnrollmentOutput(output) {
|
|
||||||
const lower = (output || "").toLowerCase();
|
|
||||||
return lower.includes("no fingers enrolled")
|
|
||||||
|| lower.includes("no fingerprints enrolled")
|
|
||||||
|| lower.includes("no prints enrolled");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasMissingFingerprintReaderOutput(output) {
|
|
||||||
const lower = (output || "").toLowerCase();
|
|
||||||
return lower.includes("no devices available")
|
|
||||||
|| lower.includes("no device available")
|
|
||||||
|| lower.includes("no devices found")
|
|
||||||
|| lower.includes("list_devices failed")
|
|
||||||
|| lower.includes("no device");
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFingerprintProbe(exitCode, output) {
|
|
||||||
if (hasEnrolledFingerprintOutput(output))
|
|
||||||
return "ready";
|
|
||||||
if (hasMissingFingerprintEnrollmentOutput(output))
|
|
||||||
return "missing_enrollment";
|
|
||||||
if (hasMissingFingerprintReaderOutput(output))
|
|
||||||
return "missing_reader";
|
|
||||||
if (exitCode === 0)
|
|
||||||
return "missing_enrollment";
|
|
||||||
if (exitCode === 127 || (output || "").includes("__missing_command__"))
|
|
||||||
return "probe_failed";
|
|
||||||
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLockFingerprintCapability(canEnable, ready, reason) {
|
|
||||||
settingsRoot.lockFingerprintCanEnable = canEnable;
|
|
||||||
settingsRoot.lockFingerprintReady = ready;
|
|
||||||
settingsRoot.lockFingerprintReason = reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLockU2fCapability(canEnable, ready, reason) {
|
|
||||||
settingsRoot.lockU2fCanEnable = canEnable;
|
|
||||||
settingsRoot.lockU2fReady = ready;
|
|
||||||
settingsRoot.lockU2fReason = reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
|
|
||||||
settingsRoot.greeterFingerprintCanEnable = canEnable;
|
|
||||||
settingsRoot.greeterFingerprintReady = ready;
|
|
||||||
settingsRoot.greeterFingerprintReason = reason;
|
|
||||||
settingsRoot.greeterFingerprintSource = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGreeterU2fCapability(canEnable, ready, reason, source) {
|
|
||||||
settingsRoot.greeterU2fCanEnable = canEnable;
|
|
||||||
settingsRoot.greeterU2fReady = ready;
|
|
||||||
settingsRoot.greeterU2fReason = reason;
|
|
||||||
settingsRoot.greeterU2fSource = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeFingerprintCapabilities() {
|
|
||||||
if (forcedFprintAvailable !== null) {
|
|
||||||
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
|
|
||||||
const source = forcedFprintAvailable ? "dms" : "none";
|
|
||||||
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
|
|
||||||
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = fingerprintProbeState;
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "ready":
|
|
||||||
setLockFingerprintCapability(true, true, "ready");
|
|
||||||
break;
|
|
||||||
case "missing_enrollment":
|
|
||||||
setLockFingerprintCapability(true, false, "missing_enrollment");
|
|
||||||
break;
|
|
||||||
case "missing_reader":
|
|
||||||
setLockFingerprintCapability(false, false, "missing_reader");
|
|
||||||
break;
|
|
||||||
case "missing_pam_support":
|
|
||||||
setLockFingerprintCapability(false, false, "missing_pam_support");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setLockFingerprintCapability(false, false, "probe_failed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (greeterPamHasFprint) {
|
|
||||||
switch (state) {
|
|
||||||
case "ready":
|
|
||||||
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
|
|
||||||
break;
|
|
||||||
case "missing_enrollment":
|
|
||||||
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
|
|
||||||
break;
|
|
||||||
case "missing_reader":
|
|
||||||
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "ready":
|
|
||||||
setGreeterFingerprintCapability(true, true, "ready", "dms");
|
|
||||||
break;
|
|
||||||
case "missing_enrollment":
|
|
||||||
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
|
|
||||||
break;
|
|
||||||
case "missing_reader":
|
|
||||||
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
|
|
||||||
break;
|
|
||||||
case "missing_pam_support":
|
|
||||||
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeU2fCapabilities() {
|
|
||||||
if (forcedU2fAvailable !== null) {
|
|
||||||
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
|
|
||||||
const source = forcedU2fAvailable ? "dms" : "none";
|
|
||||||
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
|
|
||||||
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
|
|
||||||
const lockCanEnable = lockReady || pamU2fSupportDetected;
|
|
||||||
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
|
|
||||||
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
|
|
||||||
|
|
||||||
if (greeterPamHasU2f) {
|
|
||||||
setGreeterU2fCapability(true, true, "configured_externally", "pam");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const greeterReady = homeU2fKeysDetected;
|
|
||||||
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
|
|
||||||
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
|
|
||||||
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeAuthCapabilities() {
|
|
||||||
if (!settingsRoot)
|
|
||||||
return;
|
|
||||||
recomputeFingerprintCapabilities();
|
|
||||||
recomputeU2fCapabilities();
|
|
||||||
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
|
|
||||||
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalizeFingerprintProbe() {
|
|
||||||
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
|
|
||||||
return;
|
|
||||||
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalizePamSupportProbe() {
|
|
||||||
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
|
|
||||||
return;
|
|
||||||
|
|
||||||
pamFprintSupportDetected = false;
|
|
||||||
pamU2fSupportDetected = false;
|
|
||||||
|
|
||||||
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const parts = lines[i].split(":");
|
|
||||||
if (parts.length !== 2)
|
|
||||||
continue;
|
|
||||||
if (parts[0] === "pam_fprintd.so")
|
|
||||||
pamFprintSupportDetected = parts[1] === "true";
|
|
||||||
else if (parts[0] === "pam_u2f.so")
|
|
||||||
pamU2fSupportDetected = parts[1] === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
|
|
||||||
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
|
||||||
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
property var qtToolsDetectionProcess: Process {
|
property var qtToolsDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
||||||
running: false
|
running: false
|
||||||
@@ -365,15 +31,15 @@ Singleton {
|
|||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
const lines = text.trim().split("\n");
|
var lines = text.trim().split('\n');
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (var i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
var line = lines[i];
|
||||||
if (line.startsWith("qt5ct:")) {
|
if (line.startsWith('qt5ct:')) {
|
||||||
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
|
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
|
||||||
} else if (line.startsWith("qt6ct:")) {
|
} else if (line.startsWith('qt6ct:')) {
|
||||||
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
|
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
|
||||||
} else if (line.startsWith("gtk:")) {
|
} else if (line.startsWith('gtk:')) {
|
||||||
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
|
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,181 +47,13 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var fingerprintProbeProcess: Process {
|
property var fprintdDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.fingerprintProbeOutput = text || "";
|
|
||||||
root.fingerprintProbeStreamFinished = true;
|
|
||||||
root.finalizeFingerprintProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
root.fingerprintProbeExitCode = exitCode;
|
if (!settingsRoot)
|
||||||
root.fingerprintProbeExited = true;
|
return;
|
||||||
root.finalizeFingerprintProbe();
|
settingsRoot.fprintdAvailable = (exitCode === 0);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var pamSupportDetectionProcess: Process {
|
|
||||||
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.pamSupportProbeOutput = text || "";
|
|
||||||
root.pamSupportProbeStreamFinished = true;
|
|
||||||
root.finalizePamSupportProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
root.pamSupportProbeExitCode = exitCode;
|
|
||||||
root.pamSupportProbeExited = true;
|
|
||||||
root.finalizePamSupportProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: greetdPamWatcher
|
|
||||||
path: "/etc/pam.d/greetd"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.greetdPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.greetdPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: systemAuthPamWatcher
|
|
||||||
path: "/etc/pam.d/system-auth"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.systemAuthPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.systemAuthPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: commonAuthPamWatcher
|
|
||||||
path: "/etc/pam.d/common-auth"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.commonAuthPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.commonAuthPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: passwordAuthPamWatcher
|
|
||||||
path: "/etc/pam.d/password-auth"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.passwordAuthPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.passwordAuthPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: systemLoginPamWatcher
|
|
||||||
path: "/etc/pam.d/system-login"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.systemLoginPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.systemLoginPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: systemLocalLoginPamWatcher
|
|
||||||
path: "/etc/pam.d/system-local-login"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.systemLocalLoginPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.systemLocalLoginPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: commonAuthPcPamWatcher
|
|
||||||
path: "/etc/pam.d/common-auth-pc"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.commonAuthPcPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.commonAuthPcPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: loginPamWatcher
|
|
||||||
path: "/etc/pam.d/login"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.loginPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.loginPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: dankshellU2fPamWatcher
|
|
||||||
path: "/etc/pam.d/dankshell-u2f"
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.dankshellU2fPamText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.dankshellU2fPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: u2fKeysWatcher
|
|
||||||
path: root.u2fKeysPath
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
root.u2fKeysText = text();
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.u2fKeysText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,9 +79,6 @@ var SPEC = {
|
|||||||
hiddenOutputDeviceNames: { def: [] },
|
hiddenOutputDeviceNames: { def: [] },
|
||||||
hiddenInputDeviceNames: { def: [] },
|
hiddenInputDeviceNames: { def: [] },
|
||||||
|
|
||||||
locale: { def: "", onChange: "updateLocale" },
|
|
||||||
timeLocale: { def: "" },
|
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
appDrawerLastMode: { def: "apps" },
|
appDrawerLastMode: { def: "apps" },
|
||||||
niriOverviewLastMode: { def: "apps" }
|
niriOverviewLastMode: { def: "apps" }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ var SPEC = {
|
|||||||
widgetColorMode: { def: "default" },
|
widgetColorMode: { def: "default" },
|
||||||
controlCenterTileColorMode: { def: "primary" },
|
controlCenterTileColorMode: { def: "primary" },
|
||||||
buttonColorMode: { def: "primary" },
|
buttonColorMode: { def: "primary" },
|
||||||
cornerRadius: { def: 16, onChange: "updateCompositorLayout" },
|
cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
|
||||||
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
@@ -32,7 +32,6 @@ var SPEC = {
|
|||||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
|
||||||
firstDayOfWeek: { def: -1 },
|
|
||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
showSeconds: { def: false },
|
showSeconds: { def: false },
|
||||||
padHours12Hour: { def: false },
|
padHours12Hour: { def: false },
|
||||||
@@ -47,15 +46,6 @@ var SPEC = {
|
|||||||
modalAnimationSpeed: { def: 1 },
|
modalAnimationSpeed: { def: 1 },
|
||||||
modalCustomAnimationDuration: { def: 150 },
|
modalCustomAnimationDuration: { def: 150 },
|
||||||
enableRippleEffects: { def: true },
|
enableRippleEffects: { def: true },
|
||||||
m3ElevationEnabled: { def: true },
|
|
||||||
m3ElevationIntensity: { def: 12 },
|
|
||||||
m3ElevationOpacity: { def: 30 },
|
|
||||||
m3ElevationColorMode: { def: "default" },
|
|
||||||
m3ElevationLightDirection: { def: "top" },
|
|
||||||
m3ElevationCustomColor: { def: "#000000" },
|
|
||||||
modalElevationEnabled: { def: true },
|
|
||||||
popoutElevationEnabled: { def: true },
|
|
||||||
barElevationEnabled: { def: true },
|
|
||||||
wallpaperFillMode: { def: "Fill" },
|
wallpaperFillMode: { def: "Fill" },
|
||||||
blurredWallpaperLayer: { def: false },
|
blurredWallpaperLayer: { def: false },
|
||||||
blurWallpaperOnOverview: { def: false },
|
blurWallpaperOnOverview: { def: false },
|
||||||
@@ -123,7 +113,6 @@ var SPEC = {
|
|||||||
showOccupiedWorkspacesOnly: { def: false },
|
showOccupiedWorkspacesOnly: { def: false },
|
||||||
reverseScrolling: { def: false },
|
reverseScrolling: { def: false },
|
||||||
dwlShowAllTags: { def: false },
|
dwlShowAllTags: { def: false },
|
||||||
workspaceActiveAppHighlightEnabled: { def: false },
|
|
||||||
workspaceColorMode: { def: "default" },
|
workspaceColorMode: { def: "default" },
|
||||||
workspaceOccupiedColorMode: { def: "none" },
|
workspaceOccupiedColorMode: { def: "none" },
|
||||||
workspaceUnfocusedColorMode: { def: "default" },
|
workspaceUnfocusedColorMode: { def: "default" },
|
||||||
@@ -165,17 +154,6 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
greeterRememberLastSession: { def: true },
|
|
||||||
greeterRememberLastUser: { def: true },
|
|
||||||
greeterEnableFprint: { def: false },
|
|
||||||
greeterEnableU2f: { def: false },
|
|
||||||
greeterWallpaperPath: { def: "" },
|
|
||||||
greeterUse24HourClock: { def: true },
|
|
||||||
greeterShowSeconds: { def: false },
|
|
||||||
greeterPadHours12Hour: { def: false },
|
|
||||||
greeterLockDateFormat: { def: "" },
|
|
||||||
greeterFontFamily: { def: "" },
|
|
||||||
greeterWallpaperFillMode: { def: "" },
|
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
@@ -284,19 +262,17 @@ var SPEC = {
|
|||||||
matugenTemplateKitty: { def: true },
|
matugenTemplateKitty: { def: true },
|
||||||
matugenTemplateFoot: { def: true },
|
matugenTemplateFoot: { def: true },
|
||||||
matugenTemplateAlacritty: { def: true },
|
matugenTemplateAlacritty: { def: true },
|
||||||
matugenTemplateNeovim: { def: false },
|
matugenTemplateNeovim: { def: true },
|
||||||
matugenTemplateWezterm: { def: true },
|
matugenTemplateWezterm: { def: true },
|
||||||
matugenTemplateDgop: { def: true },
|
matugenTemplateDgop: { def: true },
|
||||||
matugenTemplateKcolorscheme: { def: true },
|
matugenTemplateKcolorscheme: { def: true },
|
||||||
matugenTemplateVscode: { def: true },
|
matugenTemplateVscode: { def: true },
|
||||||
matugenTemplateEmacs: { def: true },
|
matugenTemplateEmacs: { def: true },
|
||||||
matugenTemplateZed: { def: true },
|
|
||||||
|
|
||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
dockSmartAutoHide: { def: false },
|
dockSmartAutoHide: { def: false },
|
||||||
dockGroupByApp: { def: false },
|
dockGroupByApp: { def: false },
|
||||||
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
|
||||||
dockOpenOnOverview: { def: false },
|
dockOpenOnOverview: { def: false },
|
||||||
dockPosition: { def: 1 },
|
dockPosition: { def: 1 },
|
||||||
dockSpacing: { def: 4 },
|
dockSpacing: { def: 4 },
|
||||||
@@ -341,29 +317,9 @@ var SPEC = {
|
|||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
lockFingerprintCanEnable: { def: false, persist: false },
|
|
||||||
lockFingerprintReady: { def: false, persist: false },
|
|
||||||
lockFingerprintReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterFingerprintCanEnable: { def: false, persist: false },
|
|
||||||
greeterFingerprintReady: { def: false, persist: false },
|
|
||||||
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterFingerprintSource: { def: "none", persist: false },
|
|
||||||
enableU2f: { def: false },
|
|
||||||
u2fMode: { def: "or" },
|
|
||||||
u2fAvailable: { def: false, persist: false },
|
|
||||||
lockU2fCanEnable: { def: false, persist: false },
|
|
||||||
lockU2fReady: { def: false, persist: false },
|
|
||||||
lockU2fReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterU2fCanEnable: { def: false, persist: false },
|
|
||||||
greeterU2fReady: { def: false, persist: false },
|
|
||||||
greeterU2fReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterU2fSource: { def: "none", persist: false },
|
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
lockScreenVideoEnabled: { def: false },
|
|
||||||
lockScreenVideoPath: { def: "" },
|
|
||||||
lockScreenVideoCycling: { def: false },
|
|
||||||
hideBrightnessSlider: { def: false },
|
hideBrightnessSlider: { def: false },
|
||||||
|
|
||||||
notificationTimeoutLow: { def: 5000 },
|
notificationTimeoutLow: { def: 5000 },
|
||||||
@@ -380,7 +336,6 @@ var SPEC = {
|
|||||||
notificationHistorySaveNormal: { def: true },
|
notificationHistorySaveNormal: { def: true },
|
||||||
notificationHistorySaveCritical: { def: true },
|
notificationHistorySaveCritical: { def: true },
|
||||||
notificationRules: { def: [] },
|
notificationRules: { def: [] },
|
||||||
notificationFocusedMonitor: { def: false },
|
|
||||||
|
|
||||||
osdAlwaysShowValue: { def: false },
|
osdAlwaysShowValue: { def: false },
|
||||||
osdPosition: { def: 5 },
|
osdPosition: { def: 5 },
|
||||||
@@ -470,7 +425,7 @@ var SPEC = {
|
|||||||
scrollYBehavior: "workspace",
|
scrollYBehavior: "workspace",
|
||||||
shadowIntensity: 0,
|
shadowIntensity: 0,
|
||||||
shadowOpacity: 60,
|
shadowOpacity: 60,
|
||||||
shadowColorMode: "default",
|
shadowColorMode: "text",
|
||||||
shadowCustomColor: "#000000",
|
shadowCustomColor: "#000000",
|
||||||
clickThrough: false
|
clickThrough: false
|
||||||
}], onChange: "updateBarConfigs"
|
}], onChange: "updateBarConfigs"
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ function parse(root, jsonObj) {
|
|||||||
|
|
||||||
for (var k in SPEC) {
|
for (var k in SPEC) {
|
||||||
if (k === "pluginSettings") continue;
|
if (k === "pluginSettings") continue;
|
||||||
// Runtime-only keys are never in the JSON; resetting them here
|
|
||||||
// would wipe values set by detection processes on every reload.
|
|
||||||
if (SPEC[k].persist === false) continue;
|
|
||||||
if (!(k in jsonObj)) {
|
if (!(k in jsonObj)) {
|
||||||
root[k] = SPEC[k].def;
|
root[k] = SPEC[k].def;
|
||||||
}
|
}
|
||||||
@@ -229,25 +226,6 @@ function migrateToVersion(obj, targetVersion) {
|
|||||||
settings.configVersion = 5;
|
settings.configVersion = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentVersion < 6) {
|
|
||||||
console.info("Migrating settings from version", currentVersion, "to version 6");
|
|
||||||
|
|
||||||
if (settings.barElevationEnabled === undefined) {
|
|
||||||
var legacyBars = Array.isArray(settings.barConfigs) ? settings.barConfigs : [];
|
|
||||||
var hadLegacyBarShadowEnabled = false;
|
|
||||||
for (var j = 0; j < legacyBars.length; j++) {
|
|
||||||
var legacyIntensity = Number(legacyBars[j] && legacyBars[j].shadowIntensity);
|
|
||||||
if (!isNaN(legacyIntensity) && legacyIntensity > 0) {
|
|
||||||
hadLegacyBarShadowEnabled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settings.barElevationEnabled = hadLegacyBarShadowEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.configVersion = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Services.Greetd
|
||||||
|
import qs.Common
|
||||||
import qs.Modules.Greetd
|
import qs.Modules.Greetd
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
|
|||||||
+1
-18
@@ -313,7 +313,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
|
model: SettingsData.getFilteredScreens("notifications")
|
||||||
|
|
||||||
delegate: NotificationPopupManager {
|
delegate: NotificationPopupManager {
|
||||||
modelData: item
|
modelData: item
|
||||||
@@ -365,23 +365,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyLoader {
|
|
||||||
id: wifiQRCodeModalLoader
|
|
||||||
active: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.wifiQRCodeModalLoader = wifiQRCodeModalLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
WifiQRCodeModal {
|
|
||||||
id: wifiQRCodeModalItem
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.wifiQRCodeModal = wifiQRCodeModalItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: polkitAuthModalLoader
|
id: polkitAuthModalLoader
|
||||||
active: false
|
active: false
|
||||||
|
|||||||
+31
-79
@@ -21,37 +21,11 @@ Item {
|
|||||||
required property var workspaceRenameModalLoader
|
required property var workspaceRenameModalLoader
|
||||||
required property var windowRuleModalLoader
|
required property var windowRuleModalLoader
|
||||||
|
|
||||||
function getPreferredBar(refPropertyName) {
|
function getFirstBar() {
|
||||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||||
return null;
|
return null;
|
||||||
|
const firstLoader = root.dankBarRepeater.itemAt(0);
|
||||||
const focusedScreenName = BarWidgetService.getFocusedScreenName();
|
return firstLoader ? firstLoader.item : null;
|
||||||
|
|
||||||
const loaders = Array.from({
|
|
||||||
length: root.dankBarRepeater.count
|
|
||||||
}, (_, i) => root.dankBarRepeater.itemAt(i));
|
|
||||||
|
|
||||||
let currentBar = null;
|
|
||||||
|
|
||||||
for (const loader of loaders) {
|
|
||||||
const instances = loader?.item?.barVariants?.instances || [];
|
|
||||||
for (const bar of instances) {
|
|
||||||
if (!bar)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
|
|
||||||
const hasRef = !refPropertyName || !!bar[refPropertyName];
|
|
||||||
|
|
||||||
if (hasRef) {
|
|
||||||
currentBar = bar;
|
|
||||||
|
|
||||||
if (onFocusedScreen)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentBar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
@@ -123,9 +97,9 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
const bar = root.getPreferredBar("controlCenterButtonRef");
|
const bar = root.getFirstBar();
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerControlCenter();
|
bar.triggerControlCenterOnFocusedScreen();
|
||||||
return "CONTROL_CENTER_OPEN_SUCCESS";
|
return "CONTROL_CENTER_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
return "CONTROL_CENTER_OPEN_FAILED";
|
return "CONTROL_CENTER_OPEN_FAILED";
|
||||||
@@ -140,14 +114,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
if (root.controlCenterLoader.item?.shouldBeVisible) {
|
const bar = root.getFirstBar();
|
||||||
root.controlCenterLoader.item.close();
|
|
||||||
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
const bar = root.getPreferredBar("controlCenterButtonRef");
|
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerControlCenter();
|
bar.triggerControlCenterOnFocusedScreen();
|
||||||
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
||||||
}
|
}
|
||||||
return "CONTROL_CENTER_TOGGLE_FAILED";
|
return "CONTROL_CENTER_TOGGLE_FAILED";
|
||||||
@@ -162,37 +131,27 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(tab: string): string {
|
function open(tab: string): string {
|
||||||
const bar = root.getPreferredBar("clockButtonRef");
|
root.dankDashPopoutLoader.active = true;
|
||||||
if (!bar)
|
if (root.dankDashPopoutLoader.item) {
|
||||||
return "DASH_OPEN_FAILED";
|
switch (tab.toLowerCase()) {
|
||||||
|
case "media":
|
||||||
const dash = root.dankDashPopoutLoader.item;
|
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
||||||
const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
|
break;
|
||||||
|
case "wallpaper":
|
||||||
if (!onSameScreen) {
|
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
||||||
bar.triggerWallpaperBrowser();
|
break;
|
||||||
|
case "weather":
|
||||||
|
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen);
|
||||||
|
root.dankDashPopoutLoader.item.dashVisible = true;
|
||||||
|
return "DASH_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
|
return "DASH_OPEN_FAILED";
|
||||||
if (!root.dankDashPopoutLoader.item)
|
|
||||||
return "DASH_OPEN_FAILED";
|
|
||||||
|
|
||||||
switch (tab.toLowerCase()) {
|
|
||||||
case "media":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
|
||||||
break;
|
|
||||||
case "wallpaper":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
|
||||||
break;
|
|
||||||
case "weather":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = true;
|
|
||||||
return "DASH_OPEN_SUCCESS";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
@@ -204,14 +163,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(tab: string): string {
|
function toggle(tab: string): string {
|
||||||
if (root.dankDashPopoutLoader.item?.dashVisible) {
|
const bar = root.getFirstBar();
|
||||||
root.dankDashPopoutLoader.item.dashVisible = false;
|
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||||
return "DASH_TOGGLE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
const bar = root.getPreferredBar("clockButtonRef");
|
|
||||||
if (bar) {
|
|
||||||
bar.triggerWallpaperBrowser();
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
if (root.dankDashPopoutLoader.item) {
|
||||||
switch (tab.toLowerCase()) {
|
switch (tab.toLowerCase()) {
|
||||||
case "media":
|
case "media":
|
||||||
@@ -568,9 +521,8 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function wallpaper(): string {
|
function wallpaper(): string {
|
||||||
const bar = root.getPreferredBar("clockButtonRef");
|
const bar = root.getFirstBar();
|
||||||
if (bar) {
|
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||||
bar.triggerWallpaperBrowser();
|
|
||||||
return "SUCCESS: Toggled wallpaper browser";
|
return "SUCCESS: Toggled wallpaper browser";
|
||||||
}
|
}
|
||||||
return "ERROR: Failed to toggle wallpaper browser";
|
return "ERROR: Failed to toggle wallpaper browser";
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Item {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
|
anchors.bottomMargin: modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
@@ -112,7 +112,14 @@ Item {
|
|||||||
if (index < 0 || index >= count) {
|
if (index < 0 || index >= count) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
positionViewAtIndex(index, ListView.Contain);
|
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
||||||
|
const itemY = index * itemHeight;
|
||||||
|
const itemBottom = itemY + itemHeight;
|
||||||
|
if (itemY < contentY) {
|
||||||
|
contentY = itemY;
|
||||||
|
} else if (itemBottom > contentY + height) {
|
||||||
|
contentY = itemBottom - height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
@@ -171,7 +178,14 @@ Item {
|
|||||||
if (index < 0 || index >= count) {
|
if (index < 0 || index >= count) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
positionViewAtIndex(index, ListView.Contain);
|
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
||||||
|
const itemY = index * itemHeight;
|
||||||
|
const itemBottom = itemY + itemHeight;
|
||||||
|
if (itemY < contentY) {
|
||||||
|
contentY = itemY;
|
||||||
|
} else if (itemBottom > contentY + height) {
|
||||||
|
contentY = itemBottom - height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ Item {
|
|||||||
sourceSize.height: 128
|
sourceSize.height: 128
|
||||||
|
|
||||||
function tryLoadImage() {
|
function tryLoadImage() {
|
||||||
if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
|
if (loadQueued || entryType !== "image" || cachedImageData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
thumbnailImage.loadQueued = true;
|
loadQueued = true;
|
||||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||||
modal.activeImageLoads++;
|
modal.activeImageLoads++;
|
||||||
thumbnailImage.loadImage();
|
loadImage();
|
||||||
} else {
|
} else {
|
||||||
retryTimer.restart();
|
retryTimer.restart();
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ Item {
|
|||||||
DMSService.sendRequest("clipboard.getEntry", {
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
"id": entry.id
|
"id": entry.id
|
||||||
}, function (response) {
|
}, function (response) {
|
||||||
thumbnailImage.loadQueued = false;
|
loadQueued = false;
|
||||||
if (modal.activeImageLoads > 0) {
|
if (modal.activeImageLoads > 0) {
|
||||||
modal.activeImageLoads--;
|
modal.activeImageLoads--;
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ Item {
|
|||||||
}
|
}
|
||||||
const data = response.result?.data;
|
const data = response.result?.data;
|
||||||
if (data) {
|
if (data) {
|
||||||
thumbnailImage.cachedImageData = data;
|
cachedImageData = data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ Item {
|
|||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 1
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
property bool enableShadow: true
|
property bool enableShadow: false
|
||||||
property alias modalFocusScope: focusScope
|
property alias modalFocusScope: focusScope
|
||||||
property bool shouldBeVisible: false
|
property bool shouldBeVisible: false
|
||||||
property bool shouldHaveFocus: shouldBeVisible
|
property bool shouldHaveFocus: shouldBeVisible
|
||||||
@@ -142,11 +142,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var shadowLevel: Theme.elevationLevel3
|
readonly property real shadowBuffer: 5
|
||||||
readonly property real shadowFallbackOffset: 6
|
|
||||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
|
||||||
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
|
||||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
|
||||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||||
|
|
||||||
@@ -381,16 +377,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ElevationShadow {
|
Rectangle {
|
||||||
id: modalShadowLayer
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: root.shadowLevel
|
color: root.backgroundColor
|
||||||
fallbackOffset: root.shadowFallbackOffset
|
border.color: root.borderColor
|
||||||
targetRadius: root.cornerRadius
|
border.width: root.borderWidth
|
||||||
targetColor: root.backgroundColor
|
radius: root.cornerRadius
|
||||||
borderColor: root.borderColor
|
|
||||||
borderWidth: root.borderWidth
|
|
||||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
|
|||||||
@@ -51,15 +51,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchModeChanged: {
|
|
||||||
if (searchMode === "apps") {
|
|
||||||
_loadAppCategories();
|
|
||||||
} else {
|
|
||||||
appCategory = "";
|
|
||||||
appCategories = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onSortAppsAlphabeticallyChanged() {
|
function onSortAppsAlphabeticallyChanged() {
|
||||||
@@ -74,12 +65,8 @@ Item {
|
|||||||
if (!active)
|
if (!active)
|
||||||
return;
|
return;
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
if (searchMode === "apps") {
|
if (!searchQuery && searchMode === "all")
|
||||||
_loadAppCategories();
|
|
||||||
performSearch();
|
performSearch();
|
||||||
} else if (!searchQuery && searchMode === "all") {
|
|
||||||
performSearch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,17 +162,10 @@ Item {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
property string fileSearchType: "all"
|
|
||||||
property string fileSearchExt: ""
|
|
||||||
property string fileSearchFolder: ""
|
|
||||||
property string fileSearchSort: "score"
|
|
||||||
|
|
||||||
property string pluginFilter: ""
|
property string pluginFilter: ""
|
||||||
property string activePluginName: ""
|
property string activePluginName: ""
|
||||||
property var activePluginCategories: []
|
property var activePluginCategories: []
|
||||||
property string activePluginCategory: ""
|
property string activePluginCategory: ""
|
||||||
property string appCategory: ""
|
|
||||||
property var appCategories: []
|
|
||||||
|
|
||||||
function getSectionViewMode(sectionId) {
|
function getSectionViewMode(sectionId) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
@@ -366,10 +346,6 @@ Item {
|
|||||||
previousSearchMode = "all";
|
previousSearchMode = "all";
|
||||||
autoSwitchedToFiles = false;
|
autoSwitchedToFiles = false;
|
||||||
isFileSearching = false;
|
isFileSearching = false;
|
||||||
fileSearchType = "all";
|
|
||||||
fileSearchExt = "";
|
|
||||||
fileSearchFolder = "";
|
|
||||||
fileSearchSort = "score";
|
|
||||||
sections = [];
|
sections = [];
|
||||||
flatModel = [];
|
flatModel = [];
|
||||||
selectedFlatIndex = 0;
|
selectedFlatIndex = 0;
|
||||||
@@ -379,8 +355,6 @@ Item {
|
|||||||
activePluginName = "";
|
activePluginName = "";
|
||||||
activePluginCategories = [];
|
activePluginCategories = [];
|
||||||
activePluginCategory = "";
|
activePluginCategory = "";
|
||||||
appCategory = "";
|
|
||||||
appCategories = [];
|
|
||||||
pluginFilter = "";
|
pluginFilter = "";
|
||||||
collapsedSections = {};
|
collapsedSections = {};
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
@@ -425,47 +399,6 @@ Item {
|
|||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAppCategory(category) {
|
|
||||||
if (appCategory === category)
|
|
||||||
return;
|
|
||||||
appCategory = category;
|
|
||||||
_queryDrivenSearch = true;
|
|
||||||
_clearModeCache();
|
|
||||||
performSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loadAppCategories() {
|
|
||||||
appCategories = AppSearchService.getAllCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFileSearchType(type) {
|
|
||||||
if (fileSearchType === type)
|
|
||||||
return;
|
|
||||||
fileSearchType = type;
|
|
||||||
performFileSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFileSearchExt(ext) {
|
|
||||||
if (fileSearchExt === ext)
|
|
||||||
return;
|
|
||||||
fileSearchExt = ext;
|
|
||||||
performFileSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFileSearchFolder(folder) {
|
|
||||||
if (fileSearchFolder === folder)
|
|
||||||
return;
|
|
||||||
fileSearchFolder = folder;
|
|
||||||
performFileSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFileSearchSort(sort) {
|
|
||||||
if (fileSearchSort === sort)
|
|
||||||
return;
|
|
||||||
fileSearchSort = sort;
|
|
||||||
performFileSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPluginFilter() {
|
function clearPluginFilter() {
|
||||||
if (pluginFilter) {
|
if (pluginFilter) {
|
||||||
pluginFilter = "";
|
pluginFilter = "";
|
||||||
@@ -622,9 +555,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchMode === "apps") {
|
if (searchMode === "apps") {
|
||||||
var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All");
|
|
||||||
var cachedSections = AppSearchService.getCachedDefaultSections();
|
var cachedSections = AppSearchService.getCachedDefaultSections();
|
||||||
if (cachedSections && !searchQuery && !isCategoryFiltered) {
|
if (cachedSections && !searchQuery) {
|
||||||
var modeCache = _getCachedModeData("apps");
|
var modeCache = _getCachedModeData("apps");
|
||||||
if (modeCache) {
|
if (modeCache) {
|
||||||
_applyHighlights(modeCache.sections, "");
|
_applyHighlights(modeCache.sections, "");
|
||||||
@@ -654,23 +586,9 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCategoryFiltered) {
|
var apps = searchApps(searchQuery);
|
||||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
for (var i = 0; i < apps.length; i++) {
|
||||||
for (var i = 0; i < rawApps.length; i++) {
|
allItems.push(apps[i]);
|
||||||
allItems.push(getOrTransformApp(rawApps[i]));
|
|
||||||
}
|
|
||||||
// Also include core apps (DMS Settings etc.) that match this category
|
|
||||||
var allCoreApps = AppSearchService.getCoreApps("");
|
|
||||||
for (var i = 0; i < allCoreApps.length; i++) {
|
|
||||||
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
|
|
||||||
if (coreAppCats.indexOf(appCategory) !== -1)
|
|
||||||
allItems.push(transformCoreApp(allCoreApps[i]));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var apps = searchApps(searchQuery);
|
|
||||||
for (var i = 0; i < apps.length; i++) {
|
|
||||||
allItems.push(apps[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||||
@@ -909,20 +827,10 @@ Item {
|
|||||||
var params = {
|
var params = {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
fuzzy: true,
|
fuzzy: true,
|
||||||
sort: fileSearchSort || "score",
|
sort: "score",
|
||||||
desc: true
|
desc: true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (DSearchService.supportsTypeFilter) {
|
|
||||||
params.type = (fileSearchType && fileSearchType !== "all") ? fileSearchType : "all";
|
|
||||||
}
|
|
||||||
if (fileSearchExt) {
|
|
||||||
params.ext = fileSearchExt;
|
|
||||||
}
|
|
||||||
if (fileSearchFolder) {
|
|
||||||
params.folder = fileSearchFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
DSearchService.search(fileQuery, params, function (response) {
|
DSearchService.search(fileQuery, params, function (response) {
|
||||||
isFileSearching = false;
|
isFileSearching = false;
|
||||||
if (response.error)
|
if (response.error)
|
||||||
@@ -932,73 +840,34 @@ Item {
|
|||||||
|
|
||||||
for (var i = 0; i < hits.length; i++) {
|
for (var i = 0; i < hits.length; i++) {
|
||||||
var hit = hits[i];
|
var hit = hits[i];
|
||||||
var docTypes = hit.locations?.doc_type;
|
|
||||||
var isDir = docTypes ? !!docTypes["dir"] : false;
|
|
||||||
fileItems.push(transformFileResult({
|
fileItems.push(transformFileResult({
|
||||||
path: hit.id || "",
|
path: hit.id || "",
|
||||||
score: hit.score || 0,
|
score: hit.score || 0
|
||||||
is_dir: isDir
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSections = [];
|
var fileSection = {
|
||||||
var showType = fileSearchType || "all";
|
id: "files",
|
||||||
|
title: I18n.tr("Files"),
|
||||||
if (showType === "all" && DSearchService.supportsTypeFilter) {
|
icon: "folder",
|
||||||
var onlyFiles = [];
|
priority: 4,
|
||||||
var onlyDirs = [];
|
items: fileItems,
|
||||||
for (var j = 0; j < fileItems.length; j++) {
|
collapsed: collapsedSections["files"] || false,
|
||||||
if (fileItems[j].data?.is_dir)
|
flatStartIndex: 0
|
||||||
onlyDirs.push(fileItems[j]);
|
};
|
||||||
else
|
|
||||||
onlyFiles.push(fileItems[j]);
|
|
||||||
}
|
|
||||||
if (onlyFiles.length > 0) {
|
|
||||||
fileSections.push({
|
|
||||||
id: "files",
|
|
||||||
title: I18n.tr("Files"),
|
|
||||||
icon: "insert_drive_file",
|
|
||||||
priority: 4,
|
|
||||||
items: onlyFiles,
|
|
||||||
collapsed: collapsedSections["files"] || false,
|
|
||||||
flatStartIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (onlyDirs.length > 0) {
|
|
||||||
fileSections.push({
|
|
||||||
id: "folders",
|
|
||||||
title: I18n.tr("Folders"),
|
|
||||||
icon: "folder",
|
|
||||||
priority: 4.1,
|
|
||||||
items: onlyDirs,
|
|
||||||
collapsed: collapsedSections["folders"] || false,
|
|
||||||
flatStartIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
|
|
||||||
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
|
|
||||||
if (fileItems.length > 0) {
|
|
||||||
fileSections.push({
|
|
||||||
id: "files",
|
|
||||||
title: filesTitle,
|
|
||||||
icon: filesIcon,
|
|
||||||
priority: 4,
|
|
||||||
items: fileItems,
|
|
||||||
collapsed: collapsedSections["files"] || false,
|
|
||||||
flatStartIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newSections;
|
var newSections;
|
||||||
if (searchMode === "files") {
|
if (searchMode === "files") {
|
||||||
newSections = fileSections;
|
newSections = fileItems.length > 0 ? [fileSection] : [];
|
||||||
} else {
|
} else {
|
||||||
var existingNonFile = sections.filter(function (s) {
|
var existingNonFile = sections.filter(function (s) {
|
||||||
return s.id !== "files" && s.id !== "folders";
|
return s.id !== "files";
|
||||||
});
|
});
|
||||||
newSections = existingNonFile.concat(fileSections);
|
if (fileItems.length > 0) {
|
||||||
|
newSections = existingNonFile.concat([fileSection]);
|
||||||
|
} else {
|
||||||
|
newSections = existingNonFile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
newSections.sort(function (a, b) {
|
newSections.sort(function (a, b) {
|
||||||
return a.priority - b.priority;
|
return a.priority - b.priority;
|
||||||
@@ -1006,7 +875,9 @@ Item {
|
|||||||
_applyHighlights(newSections, searchQuery);
|
_applyHighlights(newSections, searchQuery);
|
||||||
flatModel = Scorer.flattenSections(newSections);
|
flatModel = Scorer.flattenSections(newSections);
|
||||||
sections = newSections;
|
sections = newSections;
|
||||||
selectedFlatIndex = getFirstItemIndex();
|
if (selectedFlatIndex >= flatModel.length) {
|
||||||
|
selectedFlatIndex = getFirstItemIndex();
|
||||||
|
}
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1042,7 +913,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformFileResult(file) {
|
function transformFileResult(file) {
|
||||||
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
|
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectTrigger(query) {
|
function detectTrigger(query) {
|
||||||
@@ -1710,9 +1581,6 @@ Item {
|
|||||||
case "copy_path":
|
case "copy_path":
|
||||||
copyToClipboard(item.data.path);
|
copyToClipboard(item.data.path);
|
||||||
break;
|
break;
|
||||||
case "open_terminal":
|
|
||||||
openTerminal(item.data.path);
|
|
||||||
break;
|
|
||||||
case "copy":
|
case "copy":
|
||||||
copyToClipboard(item.name);
|
copyToClipboard(item.name);
|
||||||
break;
|
break;
|
||||||
@@ -1794,16 +1662,6 @@ Item {
|
|||||||
Qt.openUrlExternally("file://" + folder);
|
Qt.openUrlExternally("file://" + folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTerminal(path) {
|
|
||||||
if (!path)
|
|
||||||
return;
|
|
||||||
var terminal = Quickshell.env("TERMINAL") || "xterm";
|
|
||||||
Quickshell.execDetached({
|
|
||||||
command: [terminal],
|
|
||||||
workingDirectory: path
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToClipboard(text) {
|
function copyToClipboard(text) {
|
||||||
if (!text)
|
if (!text)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
@@ -76,7 +75,7 @@ Item {
|
|||||||
return Theme.primary;
|
return Theme.primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 1
|
||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
@@ -108,10 +107,6 @@ Item {
|
|||||||
spotlightContent.controller.activePluginId = "";
|
spotlightContent.controller.activePluginId = "";
|
||||||
spotlightContent.controller.activePluginName = "";
|
spotlightContent.controller.activePluginName = "";
|
||||||
spotlightContent.controller.pluginFilter = "";
|
spotlightContent.controller.pluginFilter = "";
|
||||||
spotlightContent.controller.fileSearchType = "all";
|
|
||||||
spotlightContent.controller.fileSearchExt = "";
|
|
||||||
spotlightContent.controller.fileSearchFolder = "";
|
|
||||||
spotlightContent.controller.fileSearchSort = "score";
|
|
||||||
spotlightContent.controller.collapsedSections = {};
|
spotlightContent.controller.collapsedSections = {};
|
||||||
spotlightContent.controller.selectedFlatIndex = 0;
|
spotlightContent.controller.selectedFlatIndex = 0;
|
||||||
spotlightContent.controller.selectedItem = null;
|
spotlightContent.controller.selectedItem = null;
|
||||||
@@ -391,16 +386,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ElevationShadow {
|
Rectangle {
|
||||||
id: launcherShadowLayer
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: Theme.elevationLevel3
|
color: root.backgroundColor
|
||||||
fallbackOffset: 6
|
border.color: root.borderColor
|
||||||
targetColor: root.backgroundColor
|
border.width: root.borderWidth
|
||||||
borderColor: root.borderColor
|
radius: root.cornerRadius
|
||||||
borderWidth: root.borderWidth
|
|
||||||
targetRadius: root.cornerRadius
|
|
||||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -116,43 +116,31 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) {
|
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
|
||||||
var filename = file.path ? file.path.split("/").pop() : "";
|
var filename = file.path ? file.path.split("/").pop() : "";
|
||||||
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
||||||
var isDir = file.is_dir || false;
|
|
||||||
|
|
||||||
var actions = [];
|
|
||||||
if (isDir) {
|
|
||||||
if (openTerminalLabel) {
|
|
||||||
actions.push({
|
|
||||||
name: openTerminalLabel,
|
|
||||||
icon: "terminal",
|
|
||||||
action: "open_terminal"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actions.push({
|
|
||||||
name: openFolderLabel,
|
|
||||||
icon: "folder_open",
|
|
||||||
action: "open_folder"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
actions.push({
|
|
||||||
name: copyPathLabel,
|
|
||||||
icon: "content_copy",
|
|
||||||
action: "copy_path"
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: file.path || "",
|
id: file.path || "",
|
||||||
type: "file",
|
type: "file",
|
||||||
name: filename,
|
name: filename,
|
||||||
subtitle: dirname,
|
subtitle: dirname,
|
||||||
icon: isDir ? "folder" : Utils.getFileIcon(filename),
|
icon: Utils.getFileIcon(filename),
|
||||||
iconType: "material",
|
iconType: "material",
|
||||||
section: "files",
|
section: "files",
|
||||||
data: file,
|
data: file,
|
||||||
actions: actions,
|
actions: [
|
||||||
|
{
|
||||||
|
name: openFolderLabel,
|
||||||
|
icon: "folder_open",
|
||||||
|
action: "open_folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: copyPathLabel,
|
||||||
|
icon: "content_copy",
|
||||||
|
action: "copy_path"
|
||||||
|
}
|
||||||
|
],
|
||||||
primaryAction: {
|
primaryAction: {
|
||||||
name: openLabel,
|
name: openLabel,
|
||||||
icon: "open_in_new",
|
icon: "open_in_new",
|
||||||
|
|||||||
@@ -496,9 +496,8 @@ FocusScope {
|
|||||||
Row {
|
Row {
|
||||||
id: categoryRow
|
id: categoryRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
readonly property bool showPluginCategories: controller.activePluginCategories.length > 0
|
height: controller.activePluginCategories.length > 0 ? 36 : 0
|
||||||
height: showPluginCategories ? 36 : 0
|
visible: controller.activePluginCategories.length > 0
|
||||||
visible: showPluginCategories
|
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
@@ -512,7 +511,6 @@ FocusScope {
|
|||||||
|
|
||||||
DankDropdown {
|
DankDropdown {
|
||||||
id: categoryDropdown
|
id: categoryDropdown
|
||||||
visible: categoryRow.showPluginCategories
|
|
||||||
width: Math.min(200, parent.width)
|
width: Math.min(200, parent.width)
|
||||||
compactMode: true
|
compactMode: true
|
||||||
dropdownWidth: 200
|
dropdownWidth: 200
|
||||||
@@ -548,155 +546,11 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: fileFilterRow
|
|
||||||
width: parent.width
|
|
||||||
height: showFileFilters ? fileFilterContent.height : 0
|
|
||||||
visible: showFileFilters
|
|
||||||
|
|
||||||
readonly property bool showFileFilters: controller.searchMode === "files"
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: fileFilterContent
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: typeChips
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
visible: DSearchService.supportsTypeFilter
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: [
|
|
||||||
{
|
|
||||||
id: "all",
|
|
||||||
label: I18n.tr("All"),
|
|
||||||
icon: "search"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "file",
|
|
||||||
label: I18n.tr("Files"),
|
|
||||||
icon: "insert_drive_file"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "dir",
|
|
||||||
label: I18n.tr("Folders"),
|
|
||||||
icon: "folder"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: chipContent.width + Theme.spacingM * 2
|
|
||||||
height: sortDropdown.height
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: controller.fileSearchType === modelData.id || chipArea.containsMouse ? Theme.primaryContainer : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: chipContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
name: modelData.icon
|
|
||||||
size: 14
|
|
||||||
color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: modelData.label
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: chipArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: controller.setFileSearchType(modelData.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 1
|
|
||||||
height: 20
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: Theme.outlineMedium
|
|
||||||
visible: typeChips.visible
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: sortDropdown
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Math.min(130, parent.width / 3)
|
|
||||||
compactMode: true
|
|
||||||
dropdownWidth: 130
|
|
||||||
popupWidth: 150
|
|
||||||
maxPopupHeight: 200
|
|
||||||
currentValue: {
|
|
||||||
switch (controller.fileSearchSort) {
|
|
||||||
case "score":
|
|
||||||
return I18n.tr("Score");
|
|
||||||
case "name":
|
|
||||||
return I18n.tr("Name");
|
|
||||||
case "modified":
|
|
||||||
return I18n.tr("Modified");
|
|
||||||
case "size":
|
|
||||||
return I18n.tr("Size");
|
|
||||||
default:
|
|
||||||
return I18n.tr("Score");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options: [I18n.tr("Score"), I18n.tr("Name"), I18n.tr("Modified"), I18n.tr("Size")]
|
|
||||||
|
|
||||||
onValueChanged: value => {
|
|
||||||
var sortMap = {};
|
|
||||||
sortMap[I18n.tr("Score")] = "score";
|
|
||||||
sortMap[I18n.tr("Name")] = "name";
|
|
||||||
sortMap[I18n.tr("Modified")] = "modified";
|
|
||||||
sortMap[I18n.tr("Size")] = "size";
|
|
||||||
controller.setFileSearchSort(sortMap[value] || "score");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: extFilterField
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Math.min(100, parent.width / 4)
|
|
||||||
height: sortDropdown.height
|
|
||||||
placeholderText: I18n.tr("ext")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
showClearButton: text.length > 0
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
controller.setFileSearchExt(text.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
|
||||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
@@ -732,9 +586,6 @@ FocusScope {
|
|||||||
function onSearchQueryRequested(query) {
|
function onSearchQueryRequested(query) {
|
||||||
searchField.text = query;
|
searchField.text = query;
|
||||||
}
|
}
|
||||||
function onModeChanged() {
|
|
||||||
extFilterField.text = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
@@ -792,7 +643,7 @@ FocusScope {
|
|||||||
Image {
|
Image {
|
||||||
width: 40
|
width: 40
|
||||||
height: 40
|
height: 40
|
||||||
source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
|
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
|
||||||
sourceSize.width: 40
|
sourceSize.width: 40
|
||||||
sourceSize.height: 40
|
sourceSize.height: 40
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ Rectangle {
|
|||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
clip: true
|
|
||||||
visible: (root.item?.subtitle ?? "").length > 0
|
visible: (root.item?.subtitle ?? "").length > 0
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
@@ -182,7 +181,7 @@ Rectangle {
|
|||||||
case "plugin":
|
case "plugin":
|
||||||
return I18n.tr("Plugin");
|
return I18n.tr("Plugin");
|
||||||
case "file":
|
case "file":
|
||||||
return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
|
return I18n.tr("File");
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user