mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 23:32:50 -04:00
Compare commits
86 Commits
anims
...
f0be36062e
| Author | SHA1 | Date | |
|---|---|---|---|
| f0be36062e | |||
| 9578d6daf9 | |||
| cc6766135d | |||
| 28c9bb0925 | |||
| 7826d827dd | |||
| 7f392acc54 | |||
| 190fd662ad | |||
| e18587c471 | |||
| ddb079b62d | |||
| e7c8d208e2 | |||
| 0e2162cf29 | |||
| 4cf9b0adc7 | |||
| 1661d32641 | |||
| aa59187403 | |||
| bb08e1233a | |||
| 5343e97ab2 | |||
| edc544df7a | |||
| a880edd9fb | |||
| 7e1d808d70 | |||
| ce93f22669 | |||
| a58037b968 | |||
| ccf0b60935 | |||
| aad7011b1c | |||
| 3bde7ef4d3 | |||
| 04555dbfa7 | |||
| 3b494aa591 | |||
| 365387c3cd | |||
| bb74a0ca4d | |||
| 9cf2ef84b7 | |||
| 46aaf5ff77 | |||
| c544bda5df | |||
| e86227f05f | |||
| 53da60e4ca | |||
| 727d9c6c22 | |||
| 908e1f600e | |||
| 270d800df2 | |||
| d445d182ea | |||
| 476256c9e7 | |||
| 06ea7373f7 | |||
| e78ba77def | |||
| 7113afe9e2 | |||
| 1a2b6524e6 | |||
| 95c4aa9e4c | |||
| 9f2518c9e1 | |||
| 76c50a654a | |||
| ded2c38551 | |||
| 772094eacd | |||
| bddc2f6295 | |||
| 25dce2961b | |||
| 653cfbe6e0 | |||
| c539311083 | |||
| 60118c5d5b | |||
| c6b9b36566 | |||
| fd5b1b7c00 | |||
| ebc77b62c8 | |||
| 2ce888581f | |||
| 0e901b6404 | |||
| 688b9076e7 | |||
| c6ec7579b6 | |||
| 9417edac8d | |||
| 6185cc79d7 | |||
| 4ecdba94c2 | |||
| a11640d840 | |||
| 177a4c4095 | |||
| 63df19ab78 | |||
| 54e0eb5979 | |||
| 185284d422 | |||
| ce240405d9 | |||
| 58b700ed0d | |||
| d436fa4920 | |||
| d58486193e | |||
| e9404eb9b6 | |||
| 0fef4d515e | |||
| 86f9cf4376 | |||
| acf63c57e8 | |||
| baa956c3a1 | |||
| bb2081a936 | |||
| c984b0b9ae | |||
| 754bf8fa3c | |||
| 7840294517 | |||
| caaee88654 | |||
| e872ddc1e7 | |||
| 1eca9b4c2c | |||
| fe5bd42e25 | |||
| 32d16d0673 | |||
| 27c26d35ab |
@@ -20,7 +20,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v6
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
# fetch-depth: 0
|
# fetch-depth: 0
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -192,12 +192,12 @@ jobs:
|
|||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -195,10 +195,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Wait before OBS upload
|
||||||
|
run: sleep 3
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
@@ -344,7 +347,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -157,12 +157,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
|
||||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
git add flake.nix
|
git add flake.nix
|
||||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
git pull --rebase origin master
|
git pull --rebase origin ${{ github.ref_name }}
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||||
else
|
else
|
||||||
echo "No changes to flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+3
-1
@@ -86,7 +86,9 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
|
||||||
|
|
||||||
|
6. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
### I18n/Localization
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ clean:
|
|||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
lint-qml:
|
||||||
|
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -76,7 +79,7 @@ install-desktop:
|
|||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entry installed"
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -130,6 +133,7 @@ help:
|
|||||||
@echo " all (default) - Build the DMS binary"
|
@echo " all (default) - Build the DMS binary"
|
||||||
@echo " build - Same as 'all'"
|
@echo " build - Same as 'all'"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
|
||||||
rev: v2.9.0
|
|
||||||
hooks:
|
|
||||||
- id: golangci-lint-fmt
|
|
||||||
require_serial: true
|
|
||||||
- id: golangci-lint-full
|
|
||||||
- id: golangci-lint-config-verify
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
- id: golangci-lint-fmt
|
||||||
|
name: golangci-lint-fmt
|
||||||
|
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
|
||||||
|
language: system
|
||||||
|
require_serial: true
|
||||||
|
types: [go]
|
||||||
|
pass_filenames: false
|
||||||
|
- id: golangci-lint-full
|
||||||
|
name: golangci-lint-full
|
||||||
|
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
|
||||||
|
language: system
|
||||||
|
require_serial: true
|
||||||
|
types: [go]
|
||||||
|
pass_filenames: false
|
||||||
|
- id: golangci-lint-config-verify
|
||||||
|
name: golangci-lint-config-verify
|
||||||
|
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
|
||||||
|
language: system
|
||||||
|
files: \.golangci\.(?:yml|yaml|toml|json)
|
||||||
|
pass_filenames: false
|
||||||
- id: go-test
|
- id: go-test
|
||||||
name: go test
|
name: go test
|
||||||
entry: go test ./...
|
entry: go test ./...
|
||||||
|
|||||||
+3
-3
@@ -63,19 +63,19 @@ endif
|
|||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
install: build
|
install:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-all: build-all
|
install-all:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-dankinstall: dankinstall
|
install-dankinstall:
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter uninstall",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
@@ -222,16 +222,19 @@ 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])
|
||||||
default:
|
case clipCopyDownload || clipCopyType == "__multi__":
|
||||||
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 {
|
||||||
@@ -257,6 +260,13 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
if currentCategory != -1 {
|
if currentCategory != -1 {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
||||||
currentCategory = r.category
|
currentCategory = r.category
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
|
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
|
||||||
|
|
||||||
if doctorVerbose && r.details != "" {
|
if doctorVerbose && r.details != "" {
|
||||||
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
|
fmt.Fprintf(&sb, " - %s\n", r.details)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("\n---\n")
|
sb.WriteString("\n---\n")
|
||||||
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
|
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
|
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
+1068
-115
File diff suppressed because it is too large
Load Diff
@@ -60,7 +60,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var setupCmd = &cobra.Command{
|
|||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
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)
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
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", "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)
|
||||||
|
}
|
||||||
+1
-10
@@ -16,19 +16,10 @@ 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")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
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,29 +11,20 @@ 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")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
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,14 +7,6 @@ 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()
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
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"
|
||||||
@@ -12,17 +14,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyOpts(data, mimeType, false, false)
|
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
|
if foreground {
|
||||||
|
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 copyServe(data, mimeType, pasteOnce)
|
return copyServeReader(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
func copyFork(data io.Reader, 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")
|
||||||
@@ -30,11 +52,15 @@ func copyFork(data []byte, 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)
|
||||||
@@ -44,16 +70,66 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := stdin.Write(data); err != nil {
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
stdin.Close()
|
stdin.Close()
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
}
|
}
|
||||||
stdin.Close()
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
|
cachedData, err := createClipboardCacheFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create clipboard cache file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cachedData.Name())
|
||||||
|
|
||||||
|
if _, err := io.Copy(cachedData, data); err != nil {
|
||||||
|
return fmt.Errorf("cache clipboard data: %w", err)
|
||||||
|
}
|
||||||
|
if err := cachedData.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close temp cache file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyServeWithWriter(func(writer io.Writer) error {
|
||||||
|
cachedFile, err := os.Open(cachedData.Name())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open temp cache file: %w", err)
|
||||||
|
}
|
||||||
|
defer cachedFile.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(writer, cachedFile); err != nil {
|
||||||
|
return fmt.Errorf("write clipboard data: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
|
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)
|
||||||
@@ -139,12 +215,18 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
sendErr := make(chan error, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
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()
|
||||||
file.Write(data)
|
if err := writeTo(file); err != nil {
|
||||||
|
select {
|
||||||
|
case sendErr <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -165,6 +247,8 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
|
case err := <-sendErr:
|
||||||
|
return err
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -440,29 +440,10 @@ 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
|
||||||
@@ -616,10 +597,16 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
// since we manually manage those dependencies
|
if pkg == "dms-shell-bin" {
|
||||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
progressChan <- InstallProgressMsg{
|
||||||
// Pre-install dependencies from .SRCINFO
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
@@ -628,19 +615,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies and makedepends explicitly
|
// Install dependencies from .SRCINFO
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
depFilter := ""
|
||||||
|
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' ' ' | sed 's/[[:space:]]*$//')
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | 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, pkg, sudoPassword))
|
`, srcinfoPath, depFilter, 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)
|
||||||
@@ -657,14 +644,6 @@ 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{
|
||||||
@@ -677,7 +656,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") // Disable compression for speed
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
||||||
|
|
||||||
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,9 +92,25 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(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 {
|
||||||
@@ -194,12 +210,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 libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
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",
|
||||||
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 libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -379,6 +395,14 @@ 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)
|
||||||
|
|
||||||
@@ -482,12 +506,30 @@ 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, ", ")))
|
||||||
|
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
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{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
Progress: 0.40,
|
Progress: startProgress,
|
||||||
Step: "Installing system packages...",
|
Step: "Installing system packages...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
@@ -495,7 +537,15 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
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,28 +484,7 @@ 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, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
|
|
||||||
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 {
|
||||||
@@ -515,26 +494,57 @@ 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 {
|
||||||
for _, pkg := range packages {
|
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
break
|
}
|
||||||
|
return 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 {
|
||||||
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, packages...)
|
args := f.dnfInstallArgs(groupPackages, minimal)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: phase,
|
||||||
Progress: 0.70,
|
Progress: groupStart,
|
||||||
Step: "Installing COPR packages...",
|
Step: step,
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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,6 +29,8 @@ 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{
|
||||||
@@ -199,35 +201,7 @@ 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 {
|
||||||
@@ -297,6 +271,10 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -327,7 +305,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); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,7 +320,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); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,9 +410,32 @@ 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 {
|
||||||
@@ -514,27 +515,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func isOpenSUSEInstallMediaURI(uri string) bool {
|
||||||
|
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, ", ")))
|
||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
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{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: phase,
|
||||||
Progress: 0.40,
|
Progress: groupStart,
|
||||||
Step: "Installing system packages...",
|
Step: step,
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
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,9 +100,7 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(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 {
|
||||||
@@ -454,21 +452,7 @@ 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 {
|
||||||
@@ -477,21 +461,59 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{"apt-get", "install", "-y"}
|
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
args = append(args, packages...)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
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 {
|
||||||
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
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{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: phase,
|
||||||
Progress: 0.70,
|
Progress: groupStart,
|
||||||
Step: "Installing PPA packages...",
|
Step: step,
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 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"),
|
||||||
|
}
|
||||||
+1138
-106
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
|
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/pilebones/go-udev/netlink"
|
"github.com/pilebones/go-udev/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
udevRecvBufSize = 8 * 1024 * 1024
|
||||||
|
udevMaxRetries = 5
|
||||||
|
udevBaseDelay = 2 * time.Second
|
||||||
|
udevMaxDelay = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type UdevMonitor struct {
|
type UdevMonitor struct {
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
rescanMutex sync.Mutex
|
rescanMutex sync.Mutex
|
||||||
@@ -29,13 +37,6 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *UdevMonitor) run(manager *Manager) {
|
func (m *UdevMonitor) run(manager *Manager) {
|
||||||
conn := &netlink.UEventConn{}
|
|
||||||
if err := conn.Connect(netlink.UdevEvent); err != nil {
|
|
||||||
log.Errorf("Failed to connect to udev netlink: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
matcher := &netlink.RuleDefinitions{
|
matcher := &netlink.RuleDefinitions{
|
||||||
Rules: []netlink.RuleDefinition{
|
Rules: []netlink.RuleDefinition{
|
||||||
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||||
@@ -48,6 +49,46 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
failures := 0
|
||||||
|
for {
|
||||||
|
if err := m.monitorLoop(manager, matcher); err != nil {
|
||||||
|
log.Errorf("Udev monitor error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
failures++
|
||||||
|
if failures > udevMaxRetries {
|
||||||
|
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
|
||||||
|
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
return
|
||||||
|
case <-time.After(delay):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
|
||||||
|
conn := &netlink.UEventConn{}
|
||||||
|
if err := conn.Connect(netlink.UdevEvent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
|
||||||
|
log.Warnf("Failed to set udev socket receive buffer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
events := make(chan netlink.UEvent)
|
events := make(chan netlink.UEvent)
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
conn.Monitor(events, errs, matcher)
|
conn.Monitor(events, errs, matcher)
|
||||||
@@ -57,10 +98,9 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-m.stop:
|
case <-m.stop:
|
||||||
return
|
return nil
|
||||||
case err := <-errs:
|
case err := <-errs:
|
||||||
log.Errorf("Udev monitor error: %v", err)
|
return err
|
||||||
return
|
|
||||||
case event := <-events:
|
case event := <-events:
|
||||||
m.handleEvent(manager, event)
|
m.handleEvent(manager, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,7 +29,13 @@ 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)
|
||||||
assert.NotNil(t, result)
|
if assert.NotNil(t, result) {
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ var dbusManager *serverDbus.Manager
|
|||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
var themeModeManager *thememode.Manager
|
var themeModeManager *thememode.Manager
|
||||||
var locationManager *location.Manager
|
var locationManager *location.Manager
|
||||||
|
var geoClientInstance geolocation.Client
|
||||||
|
|
||||||
const dbusClientID = "dms-dbus-client"
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
@@ -191,7 +192,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 +205,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,8 +386,8 @@ 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")
|
||||||
@@ -1330,6 +1331,9 @@ func cleanupManagers() {
|
|||||||
if locationManager != nil {
|
if locationManager != nil {
|
||||||
locationManager.Close()
|
locationManager.Close()
|
||||||
}
|
}
|
||||||
|
if geoClientInstance != nil {
|
||||||
|
geoClientInstance.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(printDocs bool) error {
|
func Start(printDocs bool) error {
|
||||||
@@ -1545,9 +1549,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,10 +1593,41 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := InitializeThemeModeManager(); err != nil {
|
||||||
|
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
go func() {
|
||||||
|
<-loginctlReady
|
||||||
|
if loginctlManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
themeModeManager.WatchLoginctl(loginctlManager)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
geoClient := geolocation.NewClient()
|
||||||
|
geoClientInstance = geoClient
|
||||||
|
|
||||||
|
if waylandManager != nil {
|
||||||
|
waylandManager.SetGeoClient(geoClient)
|
||||||
|
}
|
||||||
|
if themeModeManager != nil {
|
||||||
|
themeModeManager.SetGeoClient(geoClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := InitializeLocationManager(geoClient); err != nil {
|
||||||
|
log.Warnf("Location manager unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := InitializeBluezManager(); err != nil {
|
if err := InitializeBluezManager(); err != nil {
|
||||||
log.Warnf("Bluez manager unavailable: %v", err)
|
log.Warnf("Bluez manager unavailable: %v", err)
|
||||||
@@ -1624,25 +1656,6 @@ 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 {
|
|
||||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
|
||||||
} else {
|
|
||||||
notifyCapabilityChange()
|
|
||||||
go func() {
|
|
||||||
<-loginctlReady
|
|
||||||
if loginctlManager == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
themeModeManager.WatchLoginctl(loginctlManager)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type Manager 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 +54,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())
|
||||||
@@ -315,6 +314,10 @@ func (m *Manager) getConfig() Config {
|
|||||||
return m.config
|
return m.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetGeoClient(client geolocation.Client) {
|
||||||
|
m.geoClient = client
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||||
if config.Latitude != nil && config.Longitude != nil {
|
if config.Latitude != nil && config.Longitude != nil {
|
||||||
return config.Latitude, config.Longitude
|
return config.Latitude, config.Longitude
|
||||||
@@ -322,6 +325,9 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
|||||||
if !config.UseIPLocation {
|
if !config.UseIPLocation {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
if m.geoClient == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
m.locationMutex.RLock()
|
m.locationMutex.RLock()
|
||||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
|
|
||||||
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 +41,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 {
|
||||||
@@ -422,6 +421,10 @@ func (m *Manager) recalcSchedule(now time.Time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetGeoClient(client geolocation.Client) {
|
||||||
|
m.geoClient = client
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) getLocation() (*float64, *float64) {
|
func (m *Manager) getLocation() (*float64, *float64) {
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
config := m.config
|
config := m.config
|
||||||
@@ -430,7 +433,13 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
|||||||
if config.Latitude != nil && config.Longitude != nil {
|
if config.Latitude != nil && config.Longitude != nil {
|
||||||
return config.Latitude, config.Longitude
|
return config.Latitude, config.Longitude
|
||||||
}
|
}
|
||||||
if config.UseIPLocation {
|
if !config.UseIPLocation {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if m.geoClient == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
m.locationMutex.RLock()
|
m.locationMutex.RLock()
|
||||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||||
@@ -450,8 +459,6 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
|||||||
m.locationMutex.Unlock()
|
m.locationMutex.Unlock()
|
||||||
return m.cachedIPLat, m.cachedIPLon
|
return m.cachedIPLat, m.cachedIPLon
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) hasValidSchedule() bool {
|
func (m *Manager) hasValidSchedule() bool {
|
||||||
m.scheduleMutex.RLock()
|
m.scheduleMutex.RLock()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package wlcontext
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -123,6 +123,9 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consecutiveErrors := 0
|
||||||
|
const maxConsecutiveErrors = 20
|
||||||
|
|
||||||
for {
|
for {
|
||||||
sc.drainCmdQueue()
|
sc.drainCmdQueue()
|
||||||
|
|
||||||
@@ -153,9 +156,19 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
||||||
log.Errorf("Wayland connection error: %v", err)
|
consecutiveErrors++
|
||||||
|
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
|
||||||
|
|
||||||
|
if consecutiveErrors >= maxConsecutiveErrors {
|
||||||
|
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveErrors = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
|
|||||||
|
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render("Setting up configuration files...")
|
status := m.styles.Normal.Render("Setting up configuration files...")
|
||||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Show progress information
|
// Show progress information
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
|
|||||||
|
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
|
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
|
||||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
|
|||||||
if !m.packageProgress.isComplete {
|
if !m.packageProgress.isComplete {
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render(m.packageProgress.step)
|
status := m.styles.Normal.Render(m.packageProgress.step)
|
||||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Show progress bar
|
// Show progress bar
|
||||||
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
|
|||||||
|
|
||||||
for i := startIdx; i < len(allLogs); i++ {
|
for i := startIdx; i < len(allLogs); i++ {
|
||||||
if allLogs[i] != "" {
|
if allLogs[i] != "" {
|
||||||
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
|
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
|
|||||||
|
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
status := m.styles.Normal.Render("Waiting for fingerprint...")
|
status := m.styles.Normal.Render("Waiting for fingerprint...")
|
||||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
fmt.Fprintf(&b, "%s %s", spinner, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
|
|||||||
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
|
|||||||
contentStyle = contentStyle.Bold(true)
|
contentStyle = contentStyle.Bold(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
fmt.Fprintf(&b, " %s %s\n",
|
||||||
prefixStyle.Render(prefix),
|
prefixStyle.Render(prefix),
|
||||||
contentStyle.Render(content)))
|
contentStyle.Render(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
|
|||||||
} else if m.isLoading {
|
} else if m.isLoading {
|
||||||
spinner := m.spinner.View()
|
spinner := m.spinner.View()
|
||||||
loading := m.styles.Normal.Render("Detecting system...")
|
loading := m.styles.Normal.Render("Detecting system...")
|
||||||
b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading))
|
fmt.Fprintf(&b, "%s %s\n\n", spinner, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer with better visual separation
|
// Footer with better visual separation
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Recommends: niri | hyprland | sway
|
Suggests: 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,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
%global version {{{ git_repo_version }}}
|
%global version {{{ git_repo_version }}}
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
%global go_toolchain_version 1.25.7
|
%global go_toolchain_version 1.26.1
|
||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Epoch: 2
|
Epoch: 2
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ let
|
|||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
compositorPackage
|
compositorPackage
|
||||||
|
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -195,7 +196,9 @@ in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
if [ -f settings.json ]; then
|
||||||
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
|
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
||||||
|
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
|
||||||
|
cp "$theme_file" custom-theme.json
|
||||||
mv settings.json settings.orig.json
|
mv settings.json settings.orig.json
|
||||||
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
||||||
fi
|
fi
|
||||||
|
|||||||
+13
-16
@@ -2,11 +2,9 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}: let
|
||||||
let
|
|
||||||
cfg = config.programs.dank-material-shell;
|
cfg = config.programs.dank-material-shell;
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
imports = [
|
imports = [
|
||||||
./dms-rename.nix
|
./dms-rename.nix
|
||||||
];
|
];
|
||||||
@@ -16,7 +14,9 @@ in
|
|||||||
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
||||||
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
||||||
includes = {
|
includes = {
|
||||||
enable = (lib.mkEnableOption "includes for niri-flake") // {
|
enable =
|
||||||
|
(lib.mkEnableOption "includes for niri-flake")
|
||||||
|
// {
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
override = lib.mkOption {
|
override = lib.mkOption {
|
||||||
@@ -44,8 +44,10 @@ in
|
|||||||
"alttab"
|
"alttab"
|
||||||
"binds"
|
"binds"
|
||||||
"colors"
|
"colors"
|
||||||
|
"cursor"
|
||||||
"layout"
|
"layout"
|
||||||
"outputs"
|
"outputs"
|
||||||
|
"windowrules"
|
||||||
"wpblur"
|
"wpblur"
|
||||||
];
|
];
|
||||||
example = [
|
example = [
|
||||||
@@ -70,12 +72,10 @@ in
|
|||||||
let
|
let
|
||||||
cfg' = cfg.niri.includes;
|
cfg' = cfg.niri.includes;
|
||||||
|
|
||||||
withOriginalConfig =
|
withOriginalConfig = dmsFiles:
|
||||||
dmsFiles:
|
if cfg'.override
|
||||||
if cfg'.override then
|
then [cfg'.originalFileName] ++ dmsFiles
|
||||||
[ cfg'.originalFileName ] ++ dmsFiles
|
else dmsFiles ++ [cfg'.originalFileName];
|
||||||
else
|
|
||||||
dmsFiles ++ [ cfg'.originalFileName ];
|
|
||||||
|
|
||||||
fixes = map (fix: "\n${fix}") (
|
fixes = map (fix: "\n${fix}") (
|
||||||
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
||||||
@@ -86,8 +86,7 @@ in
|
|||||||
layout { border { on; }; }
|
layout { border { on; }; }
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
||||||
niri-config-dms = {
|
niri-config-dms = {
|
||||||
target = "niri/config.kdl";
|
target = "niri/config.kdl";
|
||||||
@@ -104,9 +103,7 @@ in
|
|||||||
|
|
||||||
programs.niri.settings = lib.mkMerge [
|
programs.niri.settings = lib.mkMerge [
|
||||||
(lib.mkIf cfg.niri.enableKeybinds {
|
(lib.mkIf cfg.niri.enableKeybinds {
|
||||||
binds =
|
binds = with config.lib.niri.actions; let
|
||||||
with config.lib.niri.actions;
|
|
||||||
let
|
|
||||||
dms-ipc = spawn "dms" "ipc";
|
dms-ipc = spawn "dms" "ipc";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ if [[ ! -d "distro/debian" ]]; then
|
|||||||
echo "Error: Run this script from the repository root"
|
echo "Error: Run this script from the repository root"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
|
||||||
|
osc_retry() {
|
||||||
|
local max=3 attempt=1
|
||||||
|
while true; do
|
||||||
|
if osc "$@"; then return 0; fi
|
||||||
|
((attempt >= max)) && return 1
|
||||||
|
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
|
||||||
|
sleep $((5*attempt))
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# $1 = PROJECT
|
# $1 = PROJECT
|
||||||
# $2 = PACKAGE
|
# $2 = PACKAGE
|
||||||
@@ -309,8 +322,23 @@ mkdir -p "$OBS_BASE"
|
|||||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||||
cd "$OBS_BASE"
|
cd "$OBS_BASE"
|
||||||
osc co "$OBS_PROJECT/$PACKAGE"
|
CHECKOUT_OK=false
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if osc co "$OBS_PROJECT/$PACKAGE"; then
|
||||||
|
CHECKOUT_OK=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ $attempt -lt 3 ]]; then
|
||||||
|
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
|
||||||
|
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
|
||||||
|
sleep $((5*attempt))
|
||||||
|
fi
|
||||||
|
done
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
||||||
|
echo "Error: Checkout failed after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
||||||
@@ -1064,7 +1092,7 @@ fi
|
|||||||
|
|
||||||
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
||||||
echo "==> Updating working copy"
|
echo "==> Updating working copy"
|
||||||
if ! osc up 2>/dev/null; then
|
if ! osc_retry up 2>/dev/null; then
|
||||||
echo "Error: Failed to update working copy"
|
echo "Error: Failed to update working copy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1145,7 +1173,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
|||||||
else
|
else
|
||||||
echo "==> Committing to OBS"
|
echo "==> Committing to OBS"
|
||||||
set +e
|
set +e
|
||||||
osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
||||||
COMMIT_EXIT=${PIPESTATUS[0]}
|
COMMIT_EXIT=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Recommends: niri | hyprland | sway
|
Suggests: 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.
|
||||||
|
|||||||
@@ -17,6 +17,25 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
goModVersion =
|
||||||
|
let
|
||||||
|
content = builtins.readFile ./core/go.mod;
|
||||||
|
lines = builtins.filter builtins.isString (builtins.split "\n" content);
|
||||||
|
goLines = builtins.filter (l: builtins.match "go [0-9]+\\..*" l != null) lines;
|
||||||
|
matched =
|
||||||
|
if goLines != [ ] then builtins.match "go ([0-9]+)\\.([0-9]+).*" (builtins.head goLines) else null;
|
||||||
|
in
|
||||||
|
if matched != null then
|
||||||
|
{
|
||||||
|
major = builtins.elemAt matched 0;
|
||||||
|
minor = builtins.elemAt matched 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
major = "1";
|
||||||
|
minor = "25";
|
||||||
|
};
|
||||||
|
goForPkgs = pkgs: pkgs.${"go_${goModVersion.major}_${goModVersion.minor}"};
|
||||||
forEachSystem =
|
forEachSystem =
|
||||||
fn:
|
fn:
|
||||||
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
|
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
|
||||||
@@ -72,9 +91,14 @@
|
|||||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.buildGoModule (
|
dms-shell = pkgs.lib.makeOverridable (
|
||||||
|
{
|
||||||
|
extraQtPackages ? [ ],
|
||||||
|
}:
|
||||||
|
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
|
||||||
let
|
let
|
||||||
rootSrc = ./.;
|
rootSrc = ./.;
|
||||||
|
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit version;
|
inherit version;
|
||||||
@@ -110,8 +134,8 @@
|
|||||||
|
|
||||||
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 (qmlPkgs pkgs)}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||||
|
|
||||||
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
|
||||||
@@ -141,7 +165,8 @@
|
|||||||
platforms = pkgs.lib.platforms.linux;
|
platforms = pkgs.lib.platforms.linux;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
) { };
|
||||||
|
|
||||||
quickshell = quickshell.packages.${system}.default;
|
quickshell = quickshell.packages.${system}.default;
|
||||||
|
|
||||||
@@ -181,7 +206,7 @@
|
|||||||
buildInputs =
|
buildInputs =
|
||||||
with pkgs;
|
with pkgs;
|
||||||
[
|
[
|
||||||
go_1_25
|
(goForPkgs pkgs)
|
||||||
go-mockery_2
|
go-mockery_2
|
||||||
gopls
|
gopls
|
||||||
delve
|
delve
|
||||||
|
|||||||
@@ -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)
|
||||||
qmllint **/*.qml # Lint all QML files
|
make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .`
|
||||||
./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` and `qmllint **/*.qml`
|
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 .`
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -580,14 +580,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newSettings[identifier]) {
|
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||||
newSettings[identifier] = {
|
|
||||||
"enabled": false,
|
|
||||||
"mode": "interval",
|
|
||||||
"interval": 300,
|
|
||||||
"time": "06:00"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newSettings[identifier].enabled = enabled;
|
newSettings[identifier].enabled = enabled;
|
||||||
monitorCyclingSettings = newSettings;
|
monitorCyclingSettings = newSettings;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -618,14 +611,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newSettings[identifier]) {
|
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||||
newSettings[identifier] = {
|
|
||||||
"enabled": false,
|
|
||||||
"mode": "interval",
|
|
||||||
"interval": 300,
|
|
||||||
"time": "06:00"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newSettings[identifier].mode = mode;
|
newSettings[identifier].mode = mode;
|
||||||
monitorCyclingSettings = newSettings;
|
monitorCyclingSettings = newSettings;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -656,14 +642,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newSettings[identifier]) {
|
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||||
newSettings[identifier] = {
|
|
||||||
"enabled": false,
|
|
||||||
"mode": "interval",
|
|
||||||
"interval": 300,
|
|
||||||
"time": "06:00"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newSettings[identifier].interval = interval;
|
newSettings[identifier].interval = interval;
|
||||||
monitorCyclingSettings = newSettings;
|
monitorCyclingSettings = newSettings;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -694,14 +673,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newSettings[identifier]) {
|
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||||
newSettings[identifier] = {
|
|
||||||
"enabled": false,
|
|
||||||
"mode": "interval",
|
|
||||||
"interval": 300,
|
|
||||||
"time": "06:00"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newSettings[identifier].time = time;
|
newSettings[identifier].time = time;
|
||||||
monitorCyclingSettings = newSettings;
|
monitorCyclingSettings = newSettings;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -1218,7 +1190,7 @@ Singleton {
|
|||||||
"time": "06:00"
|
"time": "06:00"
|
||||||
};
|
};
|
||||||
var value = _findMonitorValue(monitorCyclingSettings, screenName);
|
var value = _findMonitorValue(monitorCyclingSettings, screenName);
|
||||||
return value !== undefined ? value : defaults;
|
return Object.assign({}, defaults, value !== undefined ? value : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -1245,7 +1217,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
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"
|
||||||
|
|
||||||
@@ -150,6 +150,7 @@ Singleton {
|
|||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
|
|
||||||
property int firstDayOfWeek: -1
|
property int firstDayOfWeek: -1
|
||||||
|
property bool showWeekNumber: false
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
@@ -280,6 +281,7 @@ 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"
|
||||||
@@ -313,6 +315,17 @@ 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"
|
||||||
@@ -441,6 +454,11 @@ Singleton {
|
|||||||
property bool syncModeWithPortal: true
|
property bool syncModeWithPortal: true
|
||||||
property bool terminalsAlwaysDark: false
|
property bool terminalsAlwaysDark: false
|
||||||
|
|
||||||
|
property string muxType: "tmux"
|
||||||
|
property bool muxUseCustomCommand: false
|
||||||
|
property string muxCustomCommand: ""
|
||||||
|
property string muxSessionFilter: ""
|
||||||
|
|
||||||
property bool runDmsMatugenTemplates: true
|
property bool runDmsMatugenTemplates: true
|
||||||
property bool matugenTemplateGtk: true
|
property bool matugenTemplateGtk: true
|
||||||
property bool matugenTemplateNiri: true
|
property bool matugenTemplateNiri: true
|
||||||
@@ -456,18 +474,25 @@ 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: true
|
property bool matugenTemplateNeovim: false
|
||||||
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 var matugenTemplateNeovimSettings: ({
|
||||||
|
"dark": { "baseTheme": "github_dark", "harmony": 0.5 },
|
||||||
|
"light": { "baseTheme": "github_light", "harmony": 0.5 }
|
||||||
|
})
|
||||||
|
|
||||||
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
|
||||||
@@ -513,9 +538,23 @@ 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 bool enableU2f: false
|
||||||
property string u2fMode: "or"
|
property string u2fMode: "or"
|
||||||
property bool u2fAvailable: false
|
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
|
||||||
@@ -538,6 +577,7 @@ 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
|
||||||
@@ -1001,13 +1041,19 @@ 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();
|
||||||
Processes.detectFprintd();
|
refreshAuthAvailability();
|
||||||
Processes.detectU2f();
|
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1201,7 @@ Singleton {
|
|||||||
"updateCompositorLayout": updateCompositorLayout,
|
"updateCompositorLayout": updateCompositorLayout,
|
||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
@@ -1247,10 +1293,47 @@ 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() {
|
||||||
_pluginSettingsLoading = true;
|
try {
|
||||||
parsePluginSettings(pluginSettingsFile.text());
|
parsePluginSettings(pluginSettingsFile.text());
|
||||||
_pluginSettingsLoading = false;
|
} catch (e) {
|
||||||
|
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) {
|
||||||
@@ -2686,6 +2769,7 @@ 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) {
|
||||||
@@ -2694,7 +2778,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
pluginSettings = {};
|
const msg = String(error || "");
|
||||||
|
if (!_isMissingPluginSettingsError(error))
|
||||||
|
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
||||||
|
_resetPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,7 +1084,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.fontFamily;
|
return GreetdSettings.getEffectiveFontFamily();
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -1551,7 +1551,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");
|
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
|
||||||
} else {
|
} else {
|
||||||
if (!SettingsData.matugenTemplateGtk)
|
if (!SettingsData.matugenTemplateGtk)
|
||||||
skipTemplates.push("gtk");
|
skipTemplates.push("gtk");
|
||||||
@@ -1595,6 +1595,8 @@ 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(","));
|
||||||
@@ -1987,7 +1989,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
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,22 +10,352 @@ 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() {
|
||||||
fprintdDetectionProcess.running = true;
|
detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectU2f() {
|
function detectU2f() {
|
||||||
u2fDetectionProcess.running = true;
|
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
|
||||||
@@ -35,15 +365,15 @@ Singleton {
|
|||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
var lines = text.trim().split('\n');
|
const lines = text.trim().split("\n");
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const 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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,23 +381,181 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var fprintdDetectionProcess: Process {
|
property var fingerprintProbeProcess: Process {
|
||||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
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"]
|
||||||
running: false
|
running: false
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (!settingsRoot)
|
stdout: StdioCollector {
|
||||||
return;
|
onStreamFinished: {
|
||||||
settingsRoot.fprintdAvailable = (exitCode === 0);
|
root.fingerprintProbeOutput = text || "";
|
||||||
|
root.fingerprintProbeStreamFinished = true;
|
||||||
|
root.finalizeFingerprintProbe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var u2fDetectionProcess: Process {
|
|
||||||
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
|
||||||
running: false
|
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
if (!settingsRoot)
|
root.fingerprintProbeExitCode = exitCode;
|
||||||
return;
|
root.fingerprintProbeExited = true;
|
||||||
settingsRoot.u2fAvailable = (exitCode === 0);
|
root.finalizeFingerprintProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ var SPEC = {
|
|||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
|
||||||
firstDayOfWeek: { def: -1 },
|
firstDayOfWeek: { def: -1 },
|
||||||
|
showWeekNumber: { def: false },
|
||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
showSeconds: { def: false },
|
showSeconds: { def: false },
|
||||||
padHours12Hour: { def: false },
|
padHours12Hour: { def: false },
|
||||||
@@ -123,6 +124,7 @@ 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" },
|
||||||
@@ -164,6 +166,17 @@ 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" },
|
||||||
@@ -256,6 +269,11 @@ var SPEC = {
|
|||||||
syncModeWithPortal: { def: true },
|
syncModeWithPortal: { def: true },
|
||||||
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
||||||
|
|
||||||
|
muxType: { def: "tmux" },
|
||||||
|
muxUseCustomCommand: { def: false },
|
||||||
|
muxCustomCommand: { def: "" },
|
||||||
|
muxSessionFilter: { def: "" },
|
||||||
|
|
||||||
runDmsMatugenTemplates: { def: true },
|
runDmsMatugenTemplates: { def: true },
|
||||||
matugenTemplateGtk: { def: true },
|
matugenTemplateGtk: { def: true },
|
||||||
matugenTemplateNiri: { def: true },
|
matugenTemplateNiri: { def: true },
|
||||||
@@ -272,17 +290,26 @@ var SPEC = {
|
|||||||
matugenTemplateKitty: { def: true },
|
matugenTemplateKitty: { def: true },
|
||||||
matugenTemplateFoot: { def: true },
|
matugenTemplateFoot: { def: true },
|
||||||
matugenTemplateAlacritty: { def: true },
|
matugenTemplateAlacritty: { def: true },
|
||||||
matugenTemplateNeovim: { def: true },
|
matugenTemplateNeovim: { def: false },
|
||||||
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 },
|
||||||
|
|
||||||
|
matugenTemplateNeovimSettings: {
|
||||||
|
def: {
|
||||||
|
dark: { baseTheme: "github_dark", harmony: 0.5 },
|
||||||
|
light: { baseTheme: "github_light", harmony: 0.5 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
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 },
|
||||||
@@ -327,9 +354,23 @@ 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 },
|
enableU2f: { def: false },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
u2fAvailable: { def: false, persist: false },
|
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 },
|
||||||
@@ -352,6 +393,7 @@ 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 },
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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 {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
|
|||||||
import qs.Modals.Greeter
|
import qs.Modals.Greeter
|
||||||
import qs.Modals.Settings
|
import qs.Modals.Settings
|
||||||
import qs.Modals.DankLauncherV2
|
import qs.Modals.DankLauncherV2
|
||||||
|
import qs.Modals
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Modules.AppDrawer
|
import qs.Modules.AppDrawer
|
||||||
import qs.Modules.DankDash
|
import qs.Modules.DankDash
|
||||||
@@ -313,7 +314,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.getFilteredScreens("notifications")
|
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
|
||||||
|
|
||||||
delegate: NotificationPopupManager {
|
delegate: NotificationPopupManager {
|
||||||
modelData: item
|
modelData: item
|
||||||
@@ -619,6 +620,10 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MuxModal {
|
||||||
|
id: muxModal
|
||||||
|
}
|
||||||
|
|
||||||
ClipboardHistoryModal {
|
ClipboardHistoryModal {
|
||||||
id: clipboardHistoryModalPopup
|
id: clipboardHistoryModalPopup
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
layerNamespace: "dms:input-modal"
|
||||||
|
keepPopoutsOpen: true
|
||||||
|
|
||||||
|
property string inputTitle: ""
|
||||||
|
property string inputMessage: ""
|
||||||
|
property string inputPlaceholder: ""
|
||||||
|
property string inputText: ""
|
||||||
|
property string confirmButtonText: "Confirm"
|
||||||
|
property string cancelButtonText: "Cancel"
|
||||||
|
property color confirmButtonColor: Theme.primary
|
||||||
|
property var onConfirm: function (text) {}
|
||||||
|
property var onCancel: function () {}
|
||||||
|
property int selectedButton: -1
|
||||||
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
|
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||||
|
inputTitle = title || "";
|
||||||
|
inputMessage = message || "";
|
||||||
|
inputPlaceholder = "";
|
||||||
|
inputText = "";
|
||||||
|
confirmButtonText = "Confirm";
|
||||||
|
cancelButtonText = "Cancel";
|
||||||
|
confirmButtonColor = Theme.primary;
|
||||||
|
onConfirm = onConfirmCallback || ((text) => {});
|
||||||
|
onCancel = onCancelCallback || (() => {});
|
||||||
|
selectedButton = -1;
|
||||||
|
keyboardNavigation = false;
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWithOptions(options) {
|
||||||
|
inputTitle = options.title || "";
|
||||||
|
inputMessage = options.message || "";
|
||||||
|
inputPlaceholder = options.placeholder || "";
|
||||||
|
inputText = options.initialText || "";
|
||||||
|
confirmButtonText = options.confirmText || "Confirm";
|
||||||
|
cancelButtonText = options.cancelText || "Cancel";
|
||||||
|
confirmButtonColor = options.confirmColor || Theme.primary;
|
||||||
|
onConfirm = options.onConfirm || ((text) => {});
|
||||||
|
onCancel = options.onCancel || (() => {});
|
||||||
|
selectedButton = -1;
|
||||||
|
keyboardNavigation = false;
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAndClose() {
|
||||||
|
const text = inputText;
|
||||||
|
close();
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelAndClose() {
|
||||||
|
close();
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectButton() {
|
||||||
|
if (selectedButton === 0) {
|
||||||
|
cancelAndClose();
|
||||||
|
} else {
|
||||||
|
confirmAndClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
allowStacking: true
|
||||||
|
modalWidth: 350
|
||||||
|
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
|
||||||
|
enableShadow: true
|
||||||
|
shouldHaveFocus: true
|
||||||
|
onBackgroundClicked: cancelAndClose()
|
||||||
|
onOpened: {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (contentLoader.item && contentLoader.item.textInputRef) {
|
||||||
|
contentLoader.item.textInputRef.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
FocusScope {
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: mainColumn.implicitHeight
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
property alias textInputRef: textInput
|
||||||
|
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
const textFieldFocused = textInput.activeFocus;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
root.cancelAndClose();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (textFieldFocused) {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
root.selectedButton = 0;
|
||||||
|
textInput.focus = false;
|
||||||
|
} else {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
if (root.selectedButton === -1) {
|
||||||
|
root.selectedButton = 0;
|
||||||
|
} else if (root.selectedButton === 0) {
|
||||||
|
root.selectedButton = 1;
|
||||||
|
} else {
|
||||||
|
root.selectedButton = -1;
|
||||||
|
textInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Left:
|
||||||
|
if (!textFieldFocused) {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
root.selectedButton = 0;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Right:
|
||||||
|
if (!textFieldFocused) {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
root.selectedButton = 1;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
if (root.selectedButton !== -1) {
|
||||||
|
root.selectButton();
|
||||||
|
} else {
|
||||||
|
root.confirmAndClose();
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.inputTitle
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.inputMessage
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: root.inputMessage !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: root.inputMessage !== "" ? Theme.spacingL : 0
|
||||||
|
visible: root.inputMessage !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceVariantAlpha
|
||||||
|
border.color: textInput.activeFocus ? Theme.primary : "transparent"
|
||||||
|
border.width: textInput.activeFocus ? 1 : 0
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: textInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
selectionColor: Theme.primary
|
||||||
|
selectedTextColor: Theme.primaryText
|
||||||
|
clip: true
|
||||||
|
text: root.inputText
|
||||||
|
onTextChanged: root.inputText = text
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.fill: parent
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||||
|
text: root.inputPlaceholder
|
||||||
|
visible: textInput.text === "" && !textInput.activeFocus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingL * 1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (root.keyboardNavigation && root.selectedButton === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (cancelButton.containsMouse) {
|
||||||
|
return Theme.surfacePressed;
|
||||||
|
} else {
|
||||||
|
return Theme.surfaceVariantAlpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
|
||||||
|
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.cancelButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.cancelAndClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const baseColor = root.confirmButtonColor;
|
||||||
|
if (root.keyboardNavigation && root.selectedButton === 1) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
|
||||||
|
} else if (confirmButton.containsMouse) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
|
||||||
|
} else {
|
||||||
|
return baseColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
|
||||||
|
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.confirmButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.primaryText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: confirmButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.confirmAndClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -207,9 +207,12 @@ Rectangle {
|
|||||||
selectedActionIndex = 0;
|
selectedActionIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleAction() {
|
function cycleAction(reverse = false) {
|
||||||
if (actions.length > 0) {
|
if (actions.length > 0) {
|
||||||
|
if (! reverse)
|
||||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||||
|
else
|
||||||
|
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
|
||||||
ensureSelectedVisible();
|
ensureSelectedVisible();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,10 +353,13 @@ Item {
|
|||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleMode() {
|
function cycleMode(reverse = false) {
|
||||||
var modes = ["all", "apps", "files", "plugins"];
|
var modes = ["all", "apps", "files", "plugins"];
|
||||||
var currentIndex = modes.indexOf(searchMode);
|
var currentIndex = modes.indexOf(searchMode);
|
||||||
|
if (!reverse)
|
||||||
var nextIndex = (currentIndex + 1) % modes.length;
|
var nextIndex = (currentIndex + 1) % modes.length;
|
||||||
|
else
|
||||||
|
var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
|
||||||
setMode(modes[nextIndex]);
|
setMode(modes[nextIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,9 +1009,7 @@ Item {
|
|||||||
_applyHighlights(newSections, searchQuery);
|
_applyHighlights(newSections, searchQuery);
|
||||||
flatModel = Scorer.flattenSections(newSections);
|
flatModel = Scorer.flattenSections(newSections);
|
||||||
sections = newSections;
|
sections = newSections;
|
||||||
if (selectedFlatIndex >= flatModel.length) {
|
|
||||||
selectedFlatIndex = getFirstItemIndex();
|
selectedFlatIndex = getFirstItemIndex();
|
||||||
}
|
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ FocusScope {
|
|||||||
controller.selectPageUp(8);
|
controller.selectPageUp(8);
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Right:
|
case Qt.Key_Right:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.cycleMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||||
controller.selectRight();
|
controller.selectRight();
|
||||||
return;
|
return;
|
||||||
@@ -165,12 +169,25 @@ FocusScope {
|
|||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Left:
|
case Qt.Key_Left:
|
||||||
|
if (hasCtrl) {
|
||||||
|
const reverse = true;
|
||||||
|
controller.cycleMode(reverse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||||
controller.selectLeft();
|
controller.selectLeft();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_H:
|
||||||
|
if (hasCtrl) {
|
||||||
|
const reverse = true;
|
||||||
|
controller.cycleMode(reverse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
case Qt.Key_J:
|
case Qt.Key_J:
|
||||||
if (hasCtrl) {
|
if (hasCtrl) {
|
||||||
controller.selectNext();
|
controller.selectNext();
|
||||||
@@ -185,6 +202,13 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_L:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.cycleMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
case Qt.Key_N:
|
case Qt.Key_N:
|
||||||
if (hasCtrl) {
|
if (hasCtrl) {
|
||||||
controller.selectNextSection();
|
controller.selectNextSection();
|
||||||
@@ -200,13 +224,19 @@ FocusScope {
|
|||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Tab:
|
case Qt.Key_Tab:
|
||||||
if (actionPanel.hasActions) {
|
if (hasCtrl && actionPanel.hasActions) {
|
||||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
controller.selectNext();
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Backtab:
|
case Qt.Key_Backtab:
|
||||||
if (actionPanel.expanded)
|
if (hasCtrl && actionPanel.expanded) {
|
||||||
actionPanel.hide();
|
const reverse = true
|
||||||
|
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.selectPrevious();
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
case Qt.Key_Enter:
|
case Qt.Key_Enter:
|
||||||
@@ -388,7 +418,7 @@ FocusScope {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Tab " + I18n.tr("actions")
|
text: "Ctrl-Tab " + I18n.tr("actions")
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: actionPanel.hasActions
|
visible: actionPanel.hasActions
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ Item {
|
|||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "files":
|
case "files":
|
||||||
if (!DSearchService.dsearchAvailable)
|
if (!DSearchService.dsearchAvailable)
|
||||||
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
|
return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
|
||||||
if (!hasQuery)
|
if (!hasQuery)
|
||||||
return I18n.tr("Type to search files");
|
return I18n.tr("Type to search files");
|
||||||
if (root.controller.searchQuery.length < 2)
|
if (root.controller.searchQuery.length < 2)
|
||||||
|
|||||||
@@ -0,0 +1,621 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: muxModal
|
||||||
|
|
||||||
|
layerNamespace: "dms:mux"
|
||||||
|
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property string searchText: ""
|
||||||
|
property var filteredSessions: []
|
||||||
|
|
||||||
|
function updateFilteredSessions() {
|
||||||
|
var filtered = []
|
||||||
|
var lowerSearch = searchText.trim().toLowerCase()
|
||||||
|
for (var i = 0; i < MuxService.sessions.length; i++) {
|
||||||
|
var session = MuxService.sessions[i]
|
||||||
|
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
|
||||||
|
continue
|
||||||
|
filtered.push(session)
|
||||||
|
}
|
||||||
|
filteredSessions = filtered
|
||||||
|
|
||||||
|
if (selectedIndex >= filteredSessions.length) {
|
||||||
|
selectedIndex = Math.max(0, filteredSessions.length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchTextChanged: updateFilteredSessions()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: MuxService
|
||||||
|
function onSessionsChanged() {
|
||||||
|
updateFilteredSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: grab
|
||||||
|
windows: [muxModal.contentWindow]
|
||||||
|
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open()
|
||||||
|
selectedIndex = -1
|
||||||
|
searchText = ""
|
||||||
|
MuxService.refreshSessions()
|
||||||
|
shouldHaveFocus = true
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (muxPanel && muxPanel.searchField) {
|
||||||
|
muxPanel.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close()
|
||||||
|
selectedIndex = -1
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachToSession(name) {
|
||||||
|
MuxService.attachToSession(name)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameSession(name) {
|
||||||
|
inputModal.showWithOptions({
|
||||||
|
title: I18n.tr("Rename Session"),
|
||||||
|
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
|
||||||
|
initialText: name,
|
||||||
|
onConfirm: function (newName) {
|
||||||
|
MuxService.renameSession(name, newName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function killSession(name) {
|
||||||
|
confirmModal.showWithOptions({
|
||||||
|
title: I18n.tr("Kill Session"),
|
||||||
|
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
|
||||||
|
confirmText: I18n.tr("Kill"),
|
||||||
|
confirmColor: Theme.primary,
|
||||||
|
onConfirm: function () {
|
||||||
|
MuxService.killSession(name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewSession() {
|
||||||
|
inputModal.showWithOptions({
|
||||||
|
title: I18n.tr("New Session"),
|
||||||
|
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
|
||||||
|
onConfirm: function (name) {
|
||||||
|
MuxService.createSession(name)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSelected() {
|
||||||
|
if (selectedIndex === -1) {
|
||||||
|
createNewSession()
|
||||||
|
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||||
|
attachToSession(filteredSessions[selectedIndex].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
modalWidth: 600
|
||||||
|
modalHeight: 600
|
||||||
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
keepContentLoaded: true
|
||||||
|
|
||||||
|
onBackgroundClicked: hide()
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 3000
|
||||||
|
running: muxModal.shouldBeVisible
|
||||||
|
repeat: true
|
||||||
|
onTriggered: MuxService.refreshSessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
muxModal.show()
|
||||||
|
return "MUX_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
muxModal.hide()
|
||||||
|
return "MUX_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
muxModal.toggle()
|
||||||
|
return "MUX_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "mux"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backwards compatibility
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
muxModal.show()
|
||||||
|
return "TMUX_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
muxModal.hide()
|
||||||
|
return "TMUX_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
muxModal.toggle()
|
||||||
|
return "TMUX_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "tmux"
|
||||||
|
}
|
||||||
|
|
||||||
|
InputModal {
|
||||||
|
id: inputModal
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
muxModal.shouldHaveFocus = false;
|
||||||
|
muxModal.contentWindow.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (muxModal.shouldBeVisible) {
|
||||||
|
muxModal.contentWindow.visible = true;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (!muxModal.shouldBeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
muxModal.shouldHaveFocus = true;
|
||||||
|
muxModal.modalFocusScope.forceActiveFocus();
|
||||||
|
if (muxPanel.searchField) {
|
||||||
|
muxPanel.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: confirmModal
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
muxModal.shouldHaveFocus = false;
|
||||||
|
muxModal.contentWindow.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (muxModal.shouldBeVisible) {
|
||||||
|
muxModal.contentWindow.visible = true;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (!muxModal.shouldBeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
muxModal.shouldHaveFocus = true;
|
||||||
|
muxModal.modalFocusScope.forceActiveFocus();
|
||||||
|
if (muxPanel.searchField) {
|
||||||
|
muxPanel.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directContent: Item {
|
||||||
|
id: muxPanel
|
||||||
|
|
||||||
|
clip: false
|
||||||
|
|
||||||
|
property alias searchField: searchField
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
|
||||||
|
(event.key === Qt.Key_Down)) {
|
||||||
|
selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
|
||||||
|
(event.key === Qt.Key_Up)) {
|
||||||
|
selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
|
||||||
|
createNewSession()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
|
||||||
|
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||||
|
renameSession(filteredSessions[selectedIndex].name)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||||
|
killSession(filteredSessions[selectedIndex].name)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
hide()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
activateSelected()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
height: parent.height - Theme.spacingM * 2
|
||||||
|
x: Theme.spacingM
|
||||||
|
y: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search field
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
placeholderText: I18n.tr("Search sessions...")
|
||||||
|
keyForwardTargets: [muxPanel]
|
||||||
|
|
||||||
|
onTextEdited: {
|
||||||
|
muxModal.searchText = text
|
||||||
|
muxModal.selectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Session Button
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 56
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
|
||||||
|
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 20
|
||||||
|
color: Theme.primaryContainer
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "add"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("New Session")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: newMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: muxModal.createNewSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sessions List
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: muxModal.filteredSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
|
||||||
|
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sessionMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: muxModal.attachToSession(modelData.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 20
|
||||||
|
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.name.charAt(0).toUpperCase()
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: modelData.attached ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
var parts = []
|
||||||
|
if (modelData.windows !== "N/A")
|
||||||
|
parts.push(I18n.tr("%1 windows").arg(modelData.windows))
|
||||||
|
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
|
||||||
|
return parts.join(" \u2022 ")
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename button (tmux only)
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
radius: 18
|
||||||
|
visible: MuxService.supportsRename
|
||||||
|
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "edit"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: renameMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: muxModal.renameSession(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
radius: 18
|
||||||
|
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "delete"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deleteMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
muxModal.killSession(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: muxModal.filteredSessions.length === 0 ? 200 : 0
|
||||||
|
visible: muxModal.filteredSessions.length === 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
|
||||||
|
size: 48
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcuts bar
|
||||||
|
Row {
|
||||||
|
id: shortcutsBar
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
bottomPadding: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
var shortcuts = [
|
||||||
|
{ key: "↑↓", label: I18n.tr("Navigate") },
|
||||||
|
{ key: "↵", label: I18n.tr("Attach") },
|
||||||
|
{ key: "^N", label: I18n.tr("New") },
|
||||||
|
{ key: "^D", label: I18n.tr("Kill") },
|
||||||
|
{ key: "Esc", label: I18n.tr("Close") }
|
||||||
|
]
|
||||||
|
if (MuxService.supportsRename)
|
||||||
|
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
|
||||||
|
return shortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Row {
|
||||||
|
required property var modelData
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: keyText.width + Theme.spacingS
|
||||||
|
height: keyText.height + 4
|
||||||
|
radius: 4
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: keyText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.key
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.label
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -11,8 +12,45 @@ FloatingWindow {
|
|||||||
property string passwordInput: ""
|
property string passwordInput: ""
|
||||||
property var currentFlow: PolkitService.agent?.flow
|
property var currentFlow: PolkitService.agent?.flow
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool awaitingFprintForPassword: false
|
||||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||||
|
|
||||||
|
property string polkitEtcPamText: ""
|
||||||
|
property string polkitLibPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
readonly property bool polkitPamHasFprint: {
|
||||||
|
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||||
|
if (!polkitText)
|
||||||
|
return false;
|
||||||
|
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function focusPasswordField() {
|
function focusPasswordField() {
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
@@ -20,6 +58,7 @@ FloatingWindow {
|
|||||||
function show() {
|
function show() {
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
visible = true;
|
visible = true;
|
||||||
Qt.callLater(focusPasswordField);
|
Qt.callLater(focusPasswordField);
|
||||||
}
|
}
|
||||||
@@ -28,17 +67,27 @@ FloatingWindow {
|
|||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _commitSubmit() {
|
||||||
|
isLoading = true;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
|
currentFlow.submit(passwordInput);
|
||||||
|
passwordInput = "";
|
||||||
|
}
|
||||||
|
|
||||||
function submitAuth() {
|
function submitAuth() {
|
||||||
if (!currentFlow || isLoading)
|
if (!currentFlow || isLoading)
|
||||||
return;
|
return;
|
||||||
isLoading = true;
|
if (!currentFlow.isResponseRequired) {
|
||||||
currentFlow.submit(passwordInput);
|
awaitingFprintForPassword = true;
|
||||||
passwordInput = "";
|
return;
|
||||||
|
}
|
||||||
|
_commitSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAuth() {
|
function cancelAuth() {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return;
|
return;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
if (currentFlow) {
|
if (currentFlow) {
|
||||||
currentFlow.cancelAuthenticationRequest();
|
currentFlow.cancelAuthenticationRequest();
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +109,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -83,6 +133,11 @@ FloatingWindow {
|
|||||||
function onIsResponseRequiredChanged() {
|
function onIsResponseRequiredChanged() {
|
||||||
if (!currentFlow.isResponseRequired)
|
if (!currentFlow.isResponseRequired)
|
||||||
return;
|
return;
|
||||||
|
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||||
|
_commitSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
@@ -101,6 +156,41 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitEtcPamText = text()
|
||||||
|
onLoadFailed: root.polkitEtcPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/usr/lib/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitLibPamText = text()
|
||||||
|
onLoadFailed: root.polkitLibPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.systemAuthPamText = text()
|
||||||
|
onLoadFailed: root.systemAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.commonAuthPamText = text()
|
||||||
|
onLoadFailed: root.commonAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.passwordAuthPamText = text()
|
||||||
|
onLoadFailed: root.passwordAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: contentFocusScope
|
id: contentFocusScope
|
||||||
|
|
||||||
@@ -205,37 +295,31 @@ FloatingWindow {
|
|||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: inputFieldHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: passwordField.activeFocus ? 2 : 1
|
|
||||||
opacity: isLoading ? 0.5 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: !isLoading
|
|
||||||
onClicked: passwordField.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
DankTextField {
|
||||||
id: passwordField
|
id: passwordField
|
||||||
|
|
||||||
anchors.fill: parent
|
width: parent.width
|
||||||
|
height: inputFieldHeight
|
||||||
|
backgroundColor: Theme.surfaceHover
|
||||||
|
normalBorderColor: Theme.outlineStrong
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
borderWidth: 1
|
||||||
|
focusedBorderWidth: 2
|
||||||
|
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||||
|
leftIconSize: 20
|
||||||
|
leftIconColor: Theme.primary
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
opacity: isLoading ? 0.5 : 1
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
text: passwordInput
|
text: passwordInput
|
||||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||||
placeholderText: ""
|
placeholderText: ""
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: !isLoading
|
enabled: !isLoading
|
||||||
onTextEdited: passwordInput = text
|
onTextEdited: passwordInput = text
|
||||||
onAccepted: submitAuth()
|
onAccepted: submitAuth()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Authentication failed, please try again")
|
text: I18n.tr("Authentication failed, please try again")
|
||||||
|
|||||||
@@ -241,6 +241,21 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: greeterLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 31
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: GreeterTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: pluginsLoader
|
id: pluginsLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -488,5 +503,20 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: muxLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 30
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: MuxTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,6 +266,12 @@ Rectangle {
|
|||||||
"tabIndex": 8,
|
"tabIndex": 8,
|
||||||
"cupsOnly": true
|
"cupsOnly": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "multiplexers",
|
||||||
|
"text": I18n.tr("Multiplexers"),
|
||||||
|
"icon": "terminal",
|
||||||
|
"tabIndex": 30
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "window_rules",
|
"id": "window_rules",
|
||||||
"text": I18n.tr("Window Rules"),
|
"text": I18n.tr("Window Rules"),
|
||||||
@@ -287,6 +293,12 @@ Rectangle {
|
|||||||
"icon": "lock",
|
"icon": "lock",
|
||||||
"tabIndex": 11
|
"tabIndex": 11
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "greeter",
|
||||||
|
"text": I18n.tr("Greeter"),
|
||||||
|
"icon": "login",
|
||||||
|
"tabIndex": 31
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "power_sleep",
|
"id": "power_sleep",
|
||||||
"text": I18n.tr("Power & Sleep"),
|
"text": I18n.tr("Power & Sleep"),
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ DankPopout {
|
|||||||
|
|
||||||
layerNamespace: "dms:app-launcher"
|
layerNamespace: "dms:app-launcher"
|
||||||
|
|
||||||
|
readonly property real screenWidth: screen?.width ?? 1920
|
||||||
|
readonly property real screenHeight: screen?.height ?? 1080
|
||||||
|
|
||||||
property string _pendingMode: ""
|
property string _pendingMode: ""
|
||||||
property string _pendingQuery: ""
|
property string _pendingQuery: ""
|
||||||
|
|
||||||
@@ -41,8 +44,35 @@ DankPopout {
|
|||||||
openWithQuery(query);
|
openWithQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: 560
|
readonly property int _baseWidth: {
|
||||||
popupHeight: 640
|
switch (SettingsData.dankLauncherV2Size) {
|
||||||
|
case "micro":
|
||||||
|
return 500;
|
||||||
|
case "medium":
|
||||||
|
return 720;
|
||||||
|
case "large":
|
||||||
|
return 860;
|
||||||
|
default:
|
||||||
|
return 620;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int _baseHeight: {
|
||||||
|
switch (SettingsData.dankLauncherV2Size) {
|
||||||
|
case "micro":
|
||||||
|
return 480;
|
||||||
|
case "medium":
|
||||||
|
return 720;
|
||||||
|
case "large":
|
||||||
|
return 860;
|
||||||
|
default:
|
||||||
|
return 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popupWidth: Math.min(_baseWidth, screenWidth - 100)
|
||||||
|
popupHeight: Math.min(_baseHeight, screenHeight - 100)
|
||||||
|
|
||||||
triggerWidth: 40
|
triggerWidth: 40
|
||||||
positioning: ""
|
positioning: ""
|
||||||
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ DankPopout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:control-center"
|
layerNamespace: "dms:control-center"
|
||||||
fullHeightSurface: false
|
fullHeightSurface: true
|
||||||
|
|
||||||
property string expandedSection: ""
|
property string expandedSection: ""
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
@@ -70,6 +70,16 @@ DankPopout {
|
|||||||
|
|
||||||
backgroundInteractive: !anyModalOpen
|
backgroundInteractive: !anyModalOpen
|
||||||
|
|
||||||
|
onCredentialsPromptOpenChanged: {
|
||||||
|
if (credentialsPromptOpen && shouldBeVisible)
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPolkitModalOpenChanged: {
|
||||||
|
if (polkitModalOpen && shouldBeVisible)
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
customKeyboardFocus: {
|
customKeyboardFocus: {
|
||||||
if (!shouldBeVisible)
|
if (!shouldBeVisible)
|
||||||
return WlrKeyboardFocus.None;
|
return WlrKeyboardFocus.None;
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -312,7 +312,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -336,7 +336,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -360,7 +360,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -686,6 +686,7 @@ PanelWindow {
|
|||||||
onHasActivePopoutChanged: evaluateReveal()
|
onHasActivePopoutChanged: evaluateReveal()
|
||||||
|
|
||||||
function updateActivePopoutState() {
|
function updateActivePopoutState() {
|
||||||
|
if (!barWindow.screen) return;
|
||||||
const screenName = barWindow.screen.name;
|
const screenName = barWindow.screen.name;
|
||||||
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
||||||
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
@@ -38,12 +39,20 @@ BasePill {
|
|||||||
property var _vAudio: null
|
property var _vAudio: null
|
||||||
property var _vBrightness: null
|
property var _vBrightness: null
|
||||||
property var _vMic: null
|
property var _vMic: null
|
||||||
|
property var _interactionDelegates: []
|
||||||
|
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
|
||||||
|
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
|
||||||
|
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
|
||||||
|
|
||||||
|
onIsVerticalOrientationChanged: root.clearInteractionRefs()
|
||||||
|
|
||||||
onWheel: function (wheelEvent) {
|
onWheel: function (wheelEvent) {
|
||||||
const delta = wheelEvent.angleDelta.y;
|
const delta = wheelEvent.angleDelta.y;
|
||||||
if (delta === 0)
|
if (delta === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
|
||||||
const rootX = wheelEvent.x - root.leftMargin;
|
const rootX = wheelEvent.x - root.leftMargin;
|
||||||
const rootY = wheelEvent.y - root.topMargin;
|
const rootY = wheelEvent.y - root.topMargin;
|
||||||
|
|
||||||
@@ -72,6 +81,8 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRightClicked: function (rootX, rootY) {
|
onRightClicked: function (rootX, rootY) {
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
|
||||||
if (root.isVerticalOrientation && _vCol) {
|
if (root.isVerticalOrientation && _vCol) {
|
||||||
const pos = root.mapToItem(_vCol, rootX, rootY);
|
const pos = root.mapToItem(_vCol, rootX, rootY);
|
||||||
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
|
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
|
||||||
@@ -279,26 +290,142 @@ BasePill {
|
|||||||
return CupsService.getTotalJobsNum() > 0;
|
return CupsService.getTotalJobsNum() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getControlCenterIconSize() {
|
||||||
|
return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveControlCenterGroupOrder() {
|
||||||
|
const knownIds = root.defaultControlCenterGroupOrder;
|
||||||
|
const savedOrder = root.widgetData?.controlCenterGroupOrder;
|
||||||
|
const result = [];
|
||||||
|
const seen = {};
|
||||||
|
|
||||||
|
if (savedOrder && typeof savedOrder.length === "number") {
|
||||||
|
for (let i = 0; i < savedOrder.length; ++i) {
|
||||||
|
const groupId = savedOrder[i];
|
||||||
|
if (knownIds.indexOf(groupId) === -1 || seen[groupId])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
seen[groupId] = true;
|
||||||
|
result.push(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < knownIds.length; ++i) {
|
||||||
|
const groupId = knownIds[i];
|
||||||
|
if (seen[groupId])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
seen[groupId] = true;
|
||||||
|
result.push(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGroupVisible(groupId) {
|
||||||
|
switch (groupId) {
|
||||||
|
case "screenSharing":
|
||||||
|
return root.showScreenSharingIcon && NiriService.hasCasts;
|
||||||
|
case "network":
|
||||||
|
return root.showNetworkIcon && NetworkService.networkAvailable;
|
||||||
|
case "vpn":
|
||||||
|
return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
|
||||||
|
case "bluetooth":
|
||||||
|
return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
|
||||||
|
case "audio":
|
||||||
|
return root.showAudioIcon;
|
||||||
|
case "microphone":
|
||||||
|
return root.showMicIcon;
|
||||||
|
case "brightness":
|
||||||
|
return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
|
||||||
|
case "battery":
|
||||||
|
return root.showBatteryIcon && BatteryService.batteryAvailable;
|
||||||
|
case "printer":
|
||||||
|
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCompositeGroup(groupId) {
|
||||||
|
return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getControlCenterRenderModel() {
|
||||||
|
return root.effectiveControlCenterGroupOrder.map(groupId => ({
|
||||||
|
"id": groupId,
|
||||||
|
"visible": root.isGroupVisible(groupId),
|
||||||
|
"composite": root.isCompositeGroup(groupId)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInteractionRefs() {
|
||||||
|
root._hAudio = null;
|
||||||
|
root._hBrightness = null;
|
||||||
|
root._hMic = null;
|
||||||
|
root._vAudio = null;
|
||||||
|
root._vBrightness = null;
|
||||||
|
root._vMic = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerInteractionDelegate(isVertical, item) {
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (let i = 0; i < root._interactionDelegates.length; ++i) {
|
||||||
|
const entry = root._interactionDelegates[i];
|
||||||
|
if (entry && entry.item === item) {
|
||||||
|
entry.isVertical = isVertical;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root._interactionDelegates = root._interactionDelegates.concat([
|
||||||
|
{
|
||||||
|
"isVertical": isVertical,
|
||||||
|
"item": item
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterInteractionDelegate(item) {
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshInteractionRefs() {
|
||||||
|
root.clearInteractionRefs();
|
||||||
|
|
||||||
|
for (let i = 0; i < root._interactionDelegates.length; ++i) {
|
||||||
|
const entry = root._interactionDelegates[i];
|
||||||
|
const item = entry?.item;
|
||||||
|
if (!item || !item.visible)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const groupId = item.interactionGroupId;
|
||||||
|
if (entry.isVertical) {
|
||||||
|
if (groupId === "audio")
|
||||||
|
root._vAudio = item;
|
||||||
|
else if (groupId === "microphone")
|
||||||
|
root._vMic = item;
|
||||||
|
else if (groupId === "brightness")
|
||||||
|
root._vBrightness = item;
|
||||||
|
} else {
|
||||||
|
if (groupId === "audio")
|
||||||
|
root._hAudio = item;
|
||||||
|
else if (groupId === "microphone")
|
||||||
|
root._hMic = item;
|
||||||
|
else if (groupId === "brightness")
|
||||||
|
root._hBrightness = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasNoVisibleIcons() {
|
function hasNoVisibleIcons() {
|
||||||
if (root.showScreenSharingIcon && NiriService.hasCasts)
|
return !root.controlCenterRenderModel.some(entry => entry.visible);
|
||||||
return false;
|
|
||||||
if (root.showNetworkIcon && NetworkService.networkAvailable)
|
|
||||||
return false;
|
|
||||||
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
|
|
||||||
return false;
|
|
||||||
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
|
|
||||||
return false;
|
|
||||||
if (root.showAudioIcon)
|
|
||||||
return false;
|
|
||||||
if (root.showMicIcon)
|
|
||||||
return false;
|
|
||||||
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
|
|
||||||
return false;
|
|
||||||
if (root.showBatteryIcon && BatteryService.batteryAvailable)
|
|
||||||
return false;
|
|
||||||
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
@@ -309,12 +436,7 @@ BasePill {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
root._hRow = controlIndicators;
|
root._hRow = controlIndicators;
|
||||||
root._vCol = controlColumn;
|
root._vCol = controlColumn;
|
||||||
root._hAudio = audioIcon.parent;
|
root.clearInteractionRefs();
|
||||||
root._hBrightness = brightnessIcon.parent;
|
|
||||||
root._hMic = micIcon.parent;
|
|
||||||
root._vAudio = audioIconV.parent;
|
|
||||||
root._vBrightness = brightnessIconV.parent;
|
|
||||||
root._vMic = micIconV.parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -324,65 +446,89 @@ BasePill {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.controlCenterRenderModel
|
||||||
Item {
|
Item {
|
||||||
|
id: verticalGroupItem
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
property string interactionGroupId: modelData.id
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: root.vIconSize
|
height: {
|
||||||
visible: root.showScreenSharingIcon && NiriService.hasCasts
|
switch (modelData.id) {
|
||||||
|
case "audio":
|
||||||
|
return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0);
|
||||||
|
case "microphone":
|
||||||
|
return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0);
|
||||||
|
case "brightness":
|
||||||
|
return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0);
|
||||||
|
default:
|
||||||
|
return root.vIconSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visible: modelData.visible
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.registerInteractionDelegate(true, verticalGroupItem);
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (root) {
|
||||||
|
root.unregisterInteractionDelegate(verticalGroupItem);
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: root.refreshInteractionRefs()
|
||||||
|
onInteractionGroupIdChanged: {
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "screen_record"
|
|
||||||
size: root.vIconSize
|
|
||||||
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
visible: !verticalGroupItem.modelData.composite
|
||||||
|
name: {
|
||||||
|
switch (verticalGroupItem.modelData.id) {
|
||||||
|
case "screenSharing":
|
||||||
|
return "screen_record";
|
||||||
|
case "network":
|
||||||
|
return root.getNetworkIconName();
|
||||||
|
case "vpn":
|
||||||
|
return "vpn_lock";
|
||||||
|
case "bluetooth":
|
||||||
|
return "bluetooth";
|
||||||
|
case "battery":
|
||||||
|
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
|
||||||
|
case "printer":
|
||||||
|
return "print";
|
||||||
|
default:
|
||||||
|
return "settings";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize
|
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.getNetworkIconName()
|
|
||||||
size: root.vIconSize
|
size: root.vIconSize
|
||||||
color: root.getNetworkIconColor()
|
color: {
|
||||||
anchors.centerIn: parent
|
switch (verticalGroupItem.modelData.id) {
|
||||||
|
case "screenSharing":
|
||||||
|
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
|
||||||
|
case "network":
|
||||||
|
return root.getNetworkIconColor();
|
||||||
|
case "vpn":
|
||||||
|
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||||
|
case "bluetooth":
|
||||||
|
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
||||||
|
case "battery":
|
||||||
|
return root.getBatteryIconColor();
|
||||||
|
case "printer":
|
||||||
|
return Theme.primary;
|
||||||
|
default:
|
||||||
|
return Theme.widgetIconColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize
|
|
||||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "vpn_lock"
|
|
||||||
size: root.vIconSize
|
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize
|
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bluetooth"
|
|
||||||
size: root.vIconSize
|
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
|
|
||||||
visible: root.showAudioIcon
|
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: audioIconV
|
id: audioIconV
|
||||||
|
visible: verticalGroupItem.modelData.id === "audio"
|
||||||
name: root.getVolumeIconName()
|
name: root.getVolumeIconName()
|
||||||
size: root.vIconSize
|
size: root.vIconSize
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
@@ -390,25 +536,21 @@ BasePill {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: audioPercentV
|
id: audioPercentV
|
||||||
visible: root.showAudioPercent
|
visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: audioIconV.bottom
|
anchors.top: audioIconV.bottom
|
||||||
anchors.topMargin: 2
|
anchors.topMargin: 2
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
|
|
||||||
visible: root.showMicIcon
|
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: micIconV
|
id: micIconV
|
||||||
|
visible: verticalGroupItem.modelData.id === "microphone"
|
||||||
name: root.getMicIconName()
|
name: root.getMicIconName()
|
||||||
size: root.vIconSize
|
size: root.vIconSize
|
||||||
color: root.getMicIconColor()
|
color: root.getMicIconColor()
|
||||||
@@ -416,25 +558,21 @@ BasePill {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: micPercentV
|
id: micPercentV
|
||||||
visible: root.showMicPercent
|
visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: micIconV.bottom
|
anchors.top: micIconV.bottom
|
||||||
anchors.topMargin: 2
|
anchors.topMargin: 2
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
|
|
||||||
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
|
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: brightnessIconV
|
id: brightnessIconV
|
||||||
|
visible: verticalGroupItem.modelData.id === "brightness"
|
||||||
name: root.getBrightnessIconName()
|
name: root.getBrightnessIconName()
|
||||||
size: root.vIconSize
|
size: root.vIconSize
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
@@ -442,10 +580,11 @@ BasePill {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: brightnessPercentV
|
id: brightnessPercentV
|
||||||
visible: root.showBrightnessPercent
|
visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness())
|
||||||
text: Math.round(getBrightness() * 100) + "%"
|
text: Math.round(getBrightness() * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -453,31 +592,6 @@ BasePill {
|
|||||||
anchors.topMargin: 2
|
anchors.topMargin: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize
|
|
||||||
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
|
||||||
size: root.vIconSize
|
|
||||||
color: root.getBatteryIconColor()
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: root.vIconSize
|
|
||||||
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "print"
|
|
||||||
size: root.vIconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -500,149 +614,206 @@ BasePill {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankIcon {
|
Repeater {
|
||||||
name: "screen_record"
|
model: root.controlCenterRenderModel
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
|
Item {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: horizontalGroupItem
|
||||||
visible: root.showScreenSharingIcon && NiriService.hasCasts
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
property string interactionGroupId: modelData.id
|
||||||
|
|
||||||
|
width: {
|
||||||
|
switch (modelData.id) {
|
||||||
|
case "audio":
|
||||||
|
return audioGroup.width;
|
||||||
|
case "microphone":
|
||||||
|
return micGroup.width;
|
||||||
|
case "brightness":
|
||||||
|
return brightnessGroup.width;
|
||||||
|
default:
|
||||||
|
return root.getControlCenterIconSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implicitWidth: width
|
||||||
|
height: root.widgetThickness - root.horizontalPadding * 2
|
||||||
|
visible: modelData.visible
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.registerInteractionDelegate(false, horizontalGroupItem);
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (root) {
|
||||||
|
root.unregisterInteractionDelegate(horizontalGroupItem);
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: root.refreshInteractionRefs()
|
||||||
|
onInteractionGroupIdChanged: {
|
||||||
|
root.refreshInteractionRefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: networkIcon
|
id: iconOnlyItem
|
||||||
name: root.getNetworkIconName()
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
color: root.getNetworkIconColor()
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
anchors.left: parent.left
|
||||||
|
visible: !horizontalGroupItem.modelData.composite
|
||||||
|
name: {
|
||||||
|
switch (horizontalGroupItem.modelData.id) {
|
||||||
|
case "screenSharing":
|
||||||
|
return "screen_record";
|
||||||
|
case "network":
|
||||||
|
return root.getNetworkIconName();
|
||||||
|
case "vpn":
|
||||||
|
return "vpn_lock";
|
||||||
|
case "bluetooth":
|
||||||
|
return "bluetooth";
|
||||||
|
case "battery":
|
||||||
|
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
|
||||||
|
case "printer":
|
||||||
|
return "print";
|
||||||
|
default:
|
||||||
|
return "settings";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size: root.getControlCenterIconSize()
|
||||||
|
color: {
|
||||||
|
switch (horizontalGroupItem.modelData.id) {
|
||||||
|
case "screenSharing":
|
||||||
|
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
|
||||||
|
case "network":
|
||||||
|
return root.getNetworkIconColor();
|
||||||
|
case "vpn":
|
||||||
|
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||||
|
case "bluetooth":
|
||||||
|
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
||||||
|
case "battery":
|
||||||
|
return root.getBatteryIconColor();
|
||||||
|
case "printer":
|
||||||
|
return Theme.primary;
|
||||||
|
default:
|
||||||
|
return Theme.widgetIconColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: vpnIcon
|
|
||||||
name: "vpn_lock"
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: bluetoothIcon
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4
|
id: audioGroup
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
width: audioContent.implicitWidth + 2
|
||||||
|
implicitWidth: width
|
||||||
|
height: parent.height
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showAudioIcon
|
visible: horizontalGroupItem.modelData.id === "audio"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: audioContent
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: audioIcon
|
id: audioIcon
|
||||||
name: root.getVolumeIconName()
|
name: root.getVolumeIconName()
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: root.getControlCenterIconSize()
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: audioPercent
|
id: audioPercent
|
||||||
visible: root.showAudioPercent
|
visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: audioIcon.right
|
width: visible ? implicitWidth : 0
|
||||||
anchors.leftMargin: 2
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
|
id: micGroup
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
width: micContent.implicitWidth + 2
|
||||||
|
implicitWidth: width
|
||||||
|
height: parent.height
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showMicIcon
|
visible: horizontalGroupItem.modelData.id === "microphone"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: micContent
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: micIcon
|
id: micIcon
|
||||||
name: root.getMicIconName()
|
name: root.getMicIconName()
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: root.getControlCenterIconSize()
|
||||||
color: root.getMicIconColor()
|
color: root.getMicIconColor()
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: micPercent
|
id: micPercent
|
||||||
visible: root.showMicPercent
|
visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: micIcon.right
|
width: visible ? implicitWidth : 0
|
||||||
anchors.leftMargin: 2
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.implicitWidth : 0) + 4
|
id: brightnessGroup
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
width: brightnessContent.implicitWidth + 2
|
||||||
|
implicitWidth: width
|
||||||
|
height: parent.height
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
|
visible: horizontalGroupItem.modelData.id === "brightness"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: brightnessContent
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
id: brightnessIcon
|
id: brightnessIcon
|
||||||
name: root.getBrightnessIconName()
|
name: root.getBrightnessIconName()
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: root.getControlCenterIconSize()
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: brightnessPercent
|
id: brightnessPercent
|
||||||
visible: root.showBrightnessPercent
|
visible: root.showBrightnessPercent && isFinite(getBrightness())
|
||||||
text: Math.round(getBrightness() * 100) + "%"
|
text: Math.round(getBrightness() * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: brightnessIcon.right
|
width: visible ? implicitWidth : 0
|
||||||
anchors.leftMargin: 2
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: batteryIcon
|
|
||||||
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
color: root.getBatteryIconColor()
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: printerIcon
|
|
||||||
name: "print"
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "settings"
|
name: "settings"
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: root.getControlCenterIconSize()
|
||||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.hasNoVisibleIcons()
|
visible: root.hasNoVisibleIcons()
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
|
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
|
||||||
return workspaceWindows.length > 0 && activeWindow && activeWindow.title;
|
return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CompositorService.isHyprland) {
|
if (CompositorService.isHyprland) {
|
||||||
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
|
if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return activeWindow && activeWindow.title;
|
return activeWindow && (activeWindow.title || activeWindow.appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
|
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
|
||||||
@@ -211,17 +211,20 @@ BasePill {
|
|||||||
text: {
|
text: {
|
||||||
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
||||||
const appName = appText.text;
|
const appName = appText.text;
|
||||||
if (!title || !appName) {
|
|
||||||
|
if (compactMode) {
|
||||||
|
if (!title || title === appName)
|
||||||
|
return title || appName;
|
||||||
|
if (title.endsWith(appName))
|
||||||
|
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName;
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title.endsWith(" - " + appName)) {
|
if (!title || !appName)
|
||||||
return title.substring(0, title.length - (" - " + appName).length);
|
return title;
|
||||||
}
|
|
||||||
|
|
||||||
if (title.endsWith(appName)) {
|
if (title.endsWith(appName))
|
||||||
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
|
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
@@ -162,6 +161,23 @@ BasePill {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string autoBarShadowDirection: {
|
||||||
|
const edge = root.axis?.edge;
|
||||||
|
switch (edge) {
|
||||||
|
case "top":
|
||||||
|
return "top";
|
||||||
|
case "bottom":
|
||||||
|
return "bottom";
|
||||||
|
case "left":
|
||||||
|
return "left";
|
||||||
|
case "right":
|
||||||
|
return "right";
|
||||||
|
default:
|
||||||
|
return "bottom";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||||
|
|
||||||
property bool menuOpen: false
|
property bool menuOpen: false
|
||||||
property var currentTrayMenu: null
|
property var currentTrayMenu: null
|
||||||
|
|
||||||
@@ -940,13 +956,6 @@ BasePill {
|
|||||||
}
|
}
|
||||||
})(), overflowMenu.dpr)
|
})(), overflowMenu.dpr)
|
||||||
|
|
||||||
readonly property var elev: Theme.elevationLevel2
|
|
||||||
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
|
||||||
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
|
||||||
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
|
||||||
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
|
||||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
|
||||||
|
|
||||||
opacity: root.menuOpen ? 1 : 0
|
opacity: root.menuOpen ? 1 : 0
|
||||||
scale: root.menuOpen ? 1 : 0.85
|
scale: root.menuOpen ? 1 : 0.85
|
||||||
|
|
||||||
@@ -967,19 +976,14 @@ BasePill {
|
|||||||
ElevationShadow {
|
ElevationShadow {
|
||||||
id: bgShadowLayer
|
id: bgShadowLayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: menuContainer.elev
|
level: Theme.elevationLevel3
|
||||||
fallbackOffset: 4
|
direction: root.effectiveShadowDirection
|
||||||
shadowBlurPx: menuContainer.shadowBlurPx
|
fallbackOffset: 6
|
||||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
|
||||||
shadowColor: {
|
|
||||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
|
||||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
|
||||||
}
|
|
||||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
targetRadius: Theme.cornerRadius
|
targetRadius: Theme.cornerRadius
|
||||||
sourceRect.antialiasing: true
|
sourceRect.antialiasing: true
|
||||||
sourceRect.smooth: true
|
sourceRect.smooth: true
|
||||||
shadowEnabled: Theme.elevationEnabled
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
||||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||||
@@ -1402,13 +1406,6 @@ BasePill {
|
|||||||
}
|
}
|
||||||
})(), menuWindow.dpr)
|
})(), menuWindow.dpr)
|
||||||
|
|
||||||
readonly property var elev: Theme.elevationLevel2
|
|
||||||
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
|
||||||
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
|
||||||
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
|
||||||
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
|
||||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
|
||||||
|
|
||||||
opacity: menuRoot.showMenu ? 1 : 0
|
opacity: menuRoot.showMenu ? 1 : 0
|
||||||
scale: menuRoot.showMenu ? 1 : 0.85
|
scale: menuRoot.showMenu ? 1 : 0.85
|
||||||
|
|
||||||
@@ -1429,18 +1426,13 @@ BasePill {
|
|||||||
ElevationShadow {
|
ElevationShadow {
|
||||||
id: menuBgShadowLayer
|
id: menuBgShadowLayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: menuContainer.elev
|
level: Theme.elevationLevel3
|
||||||
fallbackOffset: 4
|
direction: root.effectiveShadowDirection
|
||||||
shadowBlurPx: menuContainer.shadowBlurPx
|
fallbackOffset: 6
|
||||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
|
||||||
shadowColor: {
|
|
||||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
|
||||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
|
||||||
}
|
|
||||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
targetRadius: Theme.cornerRadius
|
targetRadius: Theme.cornerRadius
|
||||||
sourceRect.antialiasing: true
|
sourceRect.antialiasing: true
|
||||||
shadowEnabled: Theme.elevationEnabled
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
||||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||||
|
|||||||
@@ -1470,6 +1470,10 @@ Item {
|
|||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: root.appIconSize
|
width: root.appIconSize
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||||
|
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||||
|
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||||
|
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: rowAppIcon
|
id: rowAppIcon
|
||||||
@@ -1485,14 +1489,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||||
font.pixelSize: parent.width * 0.45
|
font.pixelSize: parent.width * 0.45
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1503,14 +1507,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: parent.width * 0.7
|
size: parent.width * 0.7
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1523,11 +1527,12 @@ Item {
|
|||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
saturation: 0
|
saturation: 0
|
||||||
colorization: 1
|
colorization: 1
|
||||||
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
|
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
|
id: rowSteamIcon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: modelData.icon
|
source: modelData.icon
|
||||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
@@ -1538,11 +1543,21 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: root.appIconSize
|
size: root.appIconSize
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
visible: modelData.isSteamApp && !modelData.icon
|
visible: modelData.isSteamApp && !modelData.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
|
border.width: 1
|
||||||
|
border.color: focusedBorderColor
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: rowAppMouseArea
|
id: rowAppMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -1624,6 +1639,10 @@ Item {
|
|||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: root.appIconSize
|
width: root.appIconSize
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||||
|
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||||
|
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||||
|
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: colAppIcon
|
id: colAppIcon
|
||||||
@@ -1639,14 +1658,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||||
font.pixelSize: parent.width * 0.45
|
font.pixelSize: parent.width * 0.45
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1657,14 +1676,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: parent.width * 0.7
|
size: parent.width * 0.7
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1677,11 +1696,12 @@ Item {
|
|||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
saturation: 0
|
saturation: 0
|
||||||
colorization: 1
|
colorization: 1
|
||||||
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
|
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
|
id: colSteamIcon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: modelData.icon
|
source: modelData.icon
|
||||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
@@ -1692,11 +1712,21 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: root.appIconSize
|
size: root.appIconSize
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
visible: modelData.isSteamApp && !modelData.icon
|
visible: modelData.isSteamApp && !modelData.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
|
border.width: 1
|
||||||
|
border.color: focusedBorderColor
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: colAppMouseArea
|
id: colAppMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ DankPopout {
|
|||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
property int currentTabIndex: 0
|
property int currentTabIndex: 0
|
||||||
|
|
||||||
popupWidth: 700
|
popupWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||||
triggerWidth: 80
|
triggerWidth: 80
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
@@ -168,6 +168,7 @@ DankPopout {
|
|||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
|
implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2))
|
||||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
focus: true
|
focus: true
|
||||||
@@ -316,6 +317,7 @@ DankPopout {
|
|||||||
id: pages
|
id: pages
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
|
implicitWidth: currentItem && currentItem.implicitWidth > 0 ? currentItem.implicitWidth : (700 - Theme.spacingM * 2)
|
||||||
implicitHeight: {
|
implicitHeight: {
|
||||||
if (root.currentTabIndex === 0)
|
if (root.currentTabIndex === 0)
|
||||||
return overviewLoader.item?.implicitHeight ?? 410;
|
return overviewLoader.item?.implicitHeight ?? 410;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ Item {
|
|||||||
return Math.max(0, Math.min(1, calculatedRatio));
|
return Math.max(0, Math.min(1, calculatedRatio));
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitWidth: 700
|
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||||
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
|
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import qs.Widgets
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||||
|
|
||||||
property bool showEventDetails: false
|
property bool showEventDetails: false
|
||||||
property date selectedDate: systemClock.date
|
property date selectedDate: systemClock.date
|
||||||
property var selectedDateEvents: []
|
property var selectedDateEvents: []
|
||||||
@@ -41,6 +43,40 @@ Rectangle {
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWeekNumber(dateObj) {
|
||||||
|
// Set time to noon to avoid potential Daylight Saving Time related bugs
|
||||||
|
const weekStartDay = startOfWeek(dateObj);
|
||||||
|
weekStartDay.setHours(12, 0, 0, 0);
|
||||||
|
|
||||||
|
let week1Start;
|
||||||
|
|
||||||
|
if (weekStartJs() === 1) {
|
||||||
|
// ISO 8601 Standard, week start on Monday
|
||||||
|
// A week belongs to the year its Thursday falls in
|
||||||
|
// So we have to get the yearTarget from weekStartDay instead of dateObj
|
||||||
|
let yearTarget = weekStartDay;
|
||||||
|
yearTarget.setDate(yearTarget.getDate() + 3); // Monday + 3 = Thursday
|
||||||
|
|
||||||
|
// Week 1 is the week containing Jan 4th
|
||||||
|
const jan4 = new Date(yearTarget.getFullYear(), 0, 4);
|
||||||
|
week1Start = startOfWeek(jan4);
|
||||||
|
} else {
|
||||||
|
// Traditional / US Standard, week start on Sunday
|
||||||
|
// A week belongs to the year its Sunday falls in
|
||||||
|
let yearTarget = weekStartDay;
|
||||||
|
yearTarget.setDate(yearTarget.getDate() + 6); // Monday + 6 = Sunday
|
||||||
|
|
||||||
|
// Week 1 is the week containing Jan 1st
|
||||||
|
const jan1 = new Date(yearTarget.getFullYear(), 0, 1);
|
||||||
|
week1Start = startOfWeek(jan1);
|
||||||
|
}
|
||||||
|
|
||||||
|
week1Start.setHours(12, 0, 0, 0);
|
||||||
|
|
||||||
|
const diffDays = Math.round((weekStartDay.getTime() - week1Start.getTime()) / 86400000); // Number of miliseconds in a day
|
||||||
|
return Math.floor(diffDays / 7) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
function updateSelectedDateEvents() {
|
function updateSelectedDateEvents() {
|
||||||
if (CalendarService && CalendarService.khalAvailable) {
|
if (CalendarService && CalendarService.khalAvailable) {
|
||||||
const events = CalendarService.getEventsForDate(selectedDate);
|
const events = CalendarService.getEventsForDate(selectedDate);
|
||||||
@@ -151,6 +187,7 @@ Rectangle {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 28
|
height: 28
|
||||||
@@ -224,8 +261,59 @@ Rectangle {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 18
|
height: parent.height - 28 - Theme.spacingS
|
||||||
visible: !showEventDetails
|
visible: !showEventDetails
|
||||||
|
spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: weekNumberColumn
|
||||||
|
visible: SettingsData.showWeekNumber
|
||||||
|
width: SettingsData.showWeekNumber ? 28 : 0
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 18
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 18 - Theme.spacingS
|
||||||
|
columns: 1
|
||||||
|
rows: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 6
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height / 6
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: {
|
||||||
|
const rowDate = new Date(calendarGrid.firstDay);
|
||||||
|
rowDate.setDate(rowDate.getDate() + index * 7);
|
||||||
|
return root.getWeekNumber(rowDate);
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: SettingsData.showWeekNumber ? (parent.width - weekNumberColumn.width - parent.spacing) : parent.width
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 18
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: {
|
model: {
|
||||||
@@ -256,7 +344,10 @@ Rectangle {
|
|||||||
|
|
||||||
Grid {
|
Grid {
|
||||||
id: calendarGrid
|
id: calendarGrid
|
||||||
visible: !showEventDetails
|
width: parent.width
|
||||||
|
height: parent.height - 18 - Theme.spacingS
|
||||||
|
columns: 7
|
||||||
|
rows: 6
|
||||||
|
|
||||||
property date displayDate: systemClock.date
|
property date displayDate: systemClock.date
|
||||||
property date selectedDate: systemClock.date
|
property date selectedDate: systemClock.date
|
||||||
@@ -266,11 +357,6 @@ Rectangle {
|
|||||||
return startOfWeek(firstOfMonth);
|
return startOfWeek(firstOfMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 28 - 18 - Theme.spacingS * 2
|
|
||||||
columns: 7
|
|
||||||
rows: 6
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: 42
|
model: 42
|
||||||
|
|
||||||
@@ -338,6 +424,9 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
width: parent.width - Theme.spacingS * 2
|
width: parent.width - Theme.spacingS * 2
|
||||||
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
|
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Item {
|
|||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
implicitWidth: 700
|
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||||
implicitHeight: 410
|
implicitHeight: 410
|
||||||
|
|
||||||
signal switchToWeatherTab
|
signal switchToWeatherTab
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Item {
|
|||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
implicitWidth: 700
|
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||||
implicitHeight: 410
|
implicitHeight: 410
|
||||||
|
|
||||||
property string wallpaperDir: ""
|
property string wallpaperDir: ""
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Item {
|
|||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
implicitWidth: 700
|
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||||
implicitHeight: 410
|
implicitHeight: 410
|
||||||
property bool syncing: false
|
property bool syncing: false
|
||||||
property bool showHourly: false
|
property bool showHourly: false
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
@@ -133,6 +134,40 @@ Item {
|
|||||||
function getGroupedToplevels() {
|
function getGroupedToplevels() {
|
||||||
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
|
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHyprToplevelForWayland(waylandToplevel) {
|
||||||
|
if (!waylandToplevel || !CompositorService.isHyprland || !Hyprland.toplevels)
|
||||||
|
return null;
|
||||||
|
const hyprToplevels = Array.from(Hyprland.toplevels.values);
|
||||||
|
for (let i = 0; i < hyprToplevels.length; i++) {
|
||||||
|
if (hyprToplevels[i].wayland === waylandToplevel)
|
||||||
|
return hyprToplevels[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpecialWorkspaceName(waylandToplevel) {
|
||||||
|
const hyprToplevel = getHyprToplevelForWayland(waylandToplevel);
|
||||||
|
if (!hyprToplevel)
|
||||||
|
return "";
|
||||||
|
const wsName = String(hyprToplevel.lastIpcObject?.workspace?.name || hyprToplevel.workspace?.name || "");
|
||||||
|
if (!wsName.startsWith("special:"))
|
||||||
|
return "";
|
||||||
|
return wsName.slice("special:".length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSpecialWorkspaceWindow(waylandToplevel) {
|
||||||
|
if (!SettingsData.dockRestoreSpecialWorkspaceOnClick || !CompositorService.isHyprland || !waylandToplevel)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const specialName = getSpecialWorkspaceName(waylandToplevel);
|
||||||
|
if (!specialName)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Hyprland.dispatch("togglespecialworkspace " + specialName);
|
||||||
|
Qt.callLater(() => waylandToplevel.activate());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
onIsHoveredChanged: {
|
onIsHoveredChanged: {
|
||||||
if (mouseArea.pressed || dragging)
|
if (mouseArea.pressed || dragging)
|
||||||
return;
|
return;
|
||||||
@@ -276,8 +311,11 @@ Item {
|
|||||||
break;
|
break;
|
||||||
case "window":
|
case "window":
|
||||||
const windowToplevel = getToplevelObject();
|
const windowToplevel = getToplevelObject();
|
||||||
if (windowToplevel)
|
if (windowToplevel) {
|
||||||
|
if (restoreSpecialWorkspaceWindow(windowToplevel))
|
||||||
|
return;
|
||||||
windowToplevel.activate();
|
windowToplevel.activate();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "grouped":
|
case "grouped":
|
||||||
if (appData.windowCount === 0) {
|
if (appData.windowCount === 0) {
|
||||||
@@ -300,8 +338,11 @@ Item {
|
|||||||
SessionService.launchDesktopEntry(groupedEntry);
|
SessionService.launchDesktopEntry(groupedEntry);
|
||||||
} else if (appData.windowCount === 1) {
|
} else if (appData.windowCount === 1) {
|
||||||
const groupedToplevel = getToplevelObject();
|
const groupedToplevel = getToplevelObject();
|
||||||
if (groupedToplevel)
|
if (groupedToplevel) {
|
||||||
|
if (restoreSpecialWorkspaceWindow(groupedToplevel))
|
||||||
|
return;
|
||||||
groupedToplevel.activate();
|
groupedToplevel.activate();
|
||||||
|
}
|
||||||
} else if (contextMenu) {
|
} else if (contextMenu) {
|
||||||
const shouldHidePin = appData.appId === "org.quickshell";
|
const shouldHidePin = appData.appId === "org.quickshell";
|
||||||
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
|
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
function readBoolOverride(envReader, names, fallbackValue) {
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
const name = names[i];
|
||||||
|
const raw = envReader(name);
|
||||||
|
if (raw === undefined || raw === null || raw === "")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const normalized = String(raw).trim().toLowerCase();
|
||||||
|
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
||||||
|
return true;
|
||||||
|
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
||||||
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
|
||||||
|
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
||||||
|
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
||||||
|
|
||||||
property string lastSessionId: ""
|
property string lastSessionId: ""
|
||||||
property string lastSuccessfulUser: ""
|
property string lastSuccessfulUser: ""
|
||||||
@@ -49,26 +52,44 @@ Singleton {
|
|||||||
if (!content || !content.trim())
|
if (!content || !content.trim())
|
||||||
return;
|
return;
|
||||||
const memory = JSON.parse(content);
|
const memory = JSON.parse(content);
|
||||||
lastSessionId = memory.lastSessionId || "";
|
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
||||||
lastSuccessfulUser = memory.lastSuccessfulUser || "";
|
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
||||||
|
if (!rememberLastSession || !rememberLastUser)
|
||||||
|
saveMemory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd memory:", e);
|
console.warn("Failed to parse greetd memory:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMemory() {
|
function saveMemory() {
|
||||||
memoryFileView.setText(JSON.stringify({
|
let memory = {};
|
||||||
"lastSessionId": lastSessionId,
|
if (rememberLastSession && lastSessionId)
|
||||||
"lastSuccessfulUser": lastSuccessfulUser
|
memory.lastSessionId = lastSessionId;
|
||||||
}, null, 2));
|
if (rememberLastUser && lastSuccessfulUser)
|
||||||
|
memory.lastSuccessfulUser = lastSuccessfulUser;
|
||||||
|
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSessionId(id) {
|
function setLastSessionId(id) {
|
||||||
|
if (!rememberLastSession) {
|
||||||
|
if (lastSessionId !== "") {
|
||||||
|
lastSessionId = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSessionId = id || "";
|
lastSessionId = id || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSuccessfulUser(username) {
|
function setLastSuccessfulUser(username) {
|
||||||
|
if (!rememberLastUser) {
|
||||||
|
if (lastSuccessfulUser !== "") {
|
||||||
|
lastSuccessfulUser = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSuccessfulUser = username || "";
|
lastSuccessfulUser = username || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,22 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string configPath: {
|
readonly property string configPath: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/settings.json";
|
return greetCfgDir + "/settings.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string _greeterCacheDir: {
|
||||||
|
const i = root.configPath.lastIndexOf("/");
|
||||||
|
return i >= 0 ? root.configPath.substring(0, i) : "";
|
||||||
|
}
|
||||||
|
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
|
||||||
|
|
||||||
property string currentThemeName: "purple"
|
property string currentThemeName: "purple"
|
||||||
property bool settingsLoaded: false
|
property bool settingsLoaded: false
|
||||||
property string customThemeFile: ""
|
property string customThemeFile: ""
|
||||||
@@ -21,6 +28,12 @@ Singleton {
|
|||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
@@ -41,6 +54,11 @@ Singleton {
|
|||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool lockScreenShowPowerActions: true
|
property bool lockScreenShowPowerActions: true
|
||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
|
property bool rememberLastSession: true
|
||||||
|
property bool rememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
property bool powerActionConfirm: true
|
property bool powerActionConfirm: true
|
||||||
property real powerActionHoldDuration: 0.5
|
property real powerActionHoldDuration: 0.5
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||||
@@ -52,14 +70,26 @@ Singleton {
|
|||||||
|
|
||||||
function parseSettings(content) {
|
function parseSettings(content) {
|
||||||
try {
|
try {
|
||||||
|
let settings = {};
|
||||||
if (content && content.trim()) {
|
if (content && content.trim()) {
|
||||||
const settings = JSON.parse(content);
|
settings = JSON.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
|
||||||
|
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
|
||||||
|
|
||||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
||||||
|
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
|
||||||
|
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
|
||||||
|
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
|
||||||
|
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
|
||||||
|
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
|
||||||
|
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
|
||||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||||
@@ -80,6 +110,19 @@ Singleton {
|
|||||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
||||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
||||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
||||||
|
if (envRememberLastSession !== undefined) {
|
||||||
|
rememberLastSession = envRememberLastSession;
|
||||||
|
} else {
|
||||||
|
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
||||||
|
}
|
||||||
|
if (envRememberLastUser !== undefined) {
|
||||||
|
rememberLastUser = envRememberLastUser;
|
||||||
|
} else {
|
||||||
|
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
||||||
|
}
|
||||||
|
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
|
||||||
|
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
||||||
|
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
||||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||||
@@ -88,7 +131,6 @@ Singleton {
|
|||||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
||||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
||||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
||||||
settingsLoaded = true;
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
if (typeof Theme !== "undefined") {
|
||||||
if (currentThemeName === "custom" && customThemeFile) {
|
if (currentThemeName === "custom" && customThemeFile) {
|
||||||
@@ -96,22 +138,35 @@ Singleton {
|
|||||||
}
|
}
|
||||||
Theme.applyGreeterTheme(currentThemeName);
|
Theme.applyGreeterTheme(currentThemeName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd settings:", e);
|
console.warn("Failed to parse greetd settings:", e);
|
||||||
|
} finally {
|
||||||
|
settingsLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
if (use24HourClock)
|
const use24 = greeterUse24HourClock;
|
||||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
const secs = greeterShowSeconds;
|
||||||
if (padHours12Hour)
|
const pad = greeterPadHours12Hour;
|
||||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
if (use24)
|
||||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
return secs ? "hh:mm:ss" : "hh:mm";
|
||||||
|
if (pad)
|
||||||
|
return secs ? "hh:mm:ss AP" : "hh:mm AP";
|
||||||
|
return secs ? "h:mm:ss AP" : "h:mm AP";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
function getEffectiveLockDateFormat() {
|
||||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
|
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
|
||||||
|
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveWallpaperFillMode() {
|
||||||
|
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveFontFamily() {
|
||||||
|
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
function getFilteredScreens(componentId) {
|
||||||
@@ -133,5 +188,9 @@ Singleton {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
parseSettings(settingsFile.text());
|
parseSettings(settingsFile.text());
|
||||||
}
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
console.warn("Failed to load greetd settings:", error);
|
||||||
|
root.parseSettings("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,38 @@ Item {
|
|||||||
signal launchRequested
|
signal launchRequested
|
||||||
|
|
||||||
property bool weatherInitialized: false
|
property bool weatherInitialized: false
|
||||||
|
property bool awaitingExternalAuth: false
|
||||||
|
property bool pendingPasswordResponse: false
|
||||||
|
property bool passwordSubmitRequested: false
|
||||||
|
property bool cancelingExternalAuthForPassword: false
|
||||||
|
property int defaultAuthTimeoutMs: 10000
|
||||||
|
property int externalAuthTimeoutMs: 30000
|
||||||
|
property int memoryFlushDelayMs: 120
|
||||||
|
property string pendingLaunchCommand: ""
|
||||||
|
property var pendingLaunchEnv: []
|
||||||
|
property int passwordFailureCount: 0
|
||||||
|
property int passwordAttemptLimitHint: 0
|
||||||
|
property string authFeedbackMessage: ""
|
||||||
|
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 faillockConfigText: ""
|
||||||
|
property bool greeterWallpaperOverrideExists: false
|
||||||
|
property string externalAuthAutoStartedForUser: ""
|
||||||
|
property int passwordSessionTransitionRetryCount: 0
|
||||||
|
property int maxPasswordSessionTransitionRetries: 2
|
||||||
|
property bool fprintdProbeComplete: false
|
||||||
|
property bool fprintdHasDevice: false
|
||||||
|
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
|
||||||
|
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
|
||||||
|
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||||
|
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||||
|
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
|
||||||
|
|
||||||
function initWeatherService() {
|
function initWeatherService() {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
@@ -44,34 +76,492 @@ Item {
|
|||||||
WeatherService.forceRefresh();
|
WeatherService.forceRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 usesPamLockoutPolicy(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
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("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePamLineDenyValue(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return -1;
|
||||||
|
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("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFaillockDenyValue(configText) {
|
||||||
|
if (!configText)
|
||||||
|
return -1;
|
||||||
|
const lines = configText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPasswordAttemptPolicyHint() {
|
||||||
|
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
|
||||||
|
let lockoutConfigured = false;
|
||||||
|
let denyFromPam = -1;
|
||||||
|
for (let i = 0; i < pamSources.length; i++) {
|
||||||
|
const source = pamSources[i];
|
||||||
|
if (!source)
|
||||||
|
continue;
|
||||||
|
if (usesPamLockoutPolicy(source))
|
||||||
|
lockoutConfigured = true;
|
||||||
|
const denyValue = parsePamLineDenyValue(source);
|
||||||
|
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
|
||||||
|
denyFromPam = denyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lockoutConfigured) {
|
||||||
|
passwordAttemptLimitHint = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
|
||||||
|
if (denyFromConfig >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromConfig;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denyFromPam >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromPam;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pam_faillock default deny value when no explicit config is set.
|
||||||
|
passwordAttemptLimitHint = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyLockoutMessage(message) {
|
||||||
|
const lower = (message || "").toLowerCase();
|
||||||
|
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentAuthMessage() {
|
||||||
|
if (GreeterState.pamState === "error")
|
||||||
|
return "Authentication error - try again";
|
||||||
|
if (GreeterState.pamState === "max")
|
||||||
|
return "Too many failed attempts - account may be locked";
|
||||||
|
if (GreeterState.pamState === "fail") {
|
||||||
|
if (passwordAttemptLimitHint > 0) {
|
||||||
|
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
|
||||||
|
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
|
||||||
|
}
|
||||||
|
return "Incorrect password - next failures may trigger account lockout";
|
||||||
|
}
|
||||||
|
return "Incorrect password";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuthFeedback() {
|
||||||
|
GreeterState.pamState = "";
|
||||||
|
authFeedbackMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPasswordSessionTransition(clearSubmitRequest) {
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
passwordSessionTransitionRetryCount = 0;
|
||||||
|
if (clearSubmitRequest)
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GreetdSettings
|
target: GreetdSettings
|
||||||
function onSettingsLoadedChanged() {
|
function onSettingsLoadedChanged() {
|
||||||
if (GreetdSettings.settingsLoaded)
|
if (GreetdSettings.settingsLoaded) {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
if (isPrimaryScreen) {
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastUserChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastSessionChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greetdPamWatcher
|
||||||
|
path: "/etc/pam.d/greetd"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.greetdPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.greetdPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: passwordAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.passwordAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.passwordAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLoginPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLoginPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLocalLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-local-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLocalLoginPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLocalLoginPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPcPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth-pc"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPcPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPcPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: loginPamWatcher
|
||||||
|
path: "/etc/pam.d/login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.loginPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.loginPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: faillockConfigWatcher
|
||||||
|
path: "/etc/security/faillock.conf"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.faillockConfigText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.faillockConfigText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
refreshPasswordAttemptPolicyHint();
|
||||||
|
|
||||||
if (isPrimaryScreen)
|
if (isPrimaryScreen)
|
||||||
applyLastSuccessfulUser();
|
applyLastSuccessfulUser();
|
||||||
|
|
||||||
if (CompositorService.isHyprland)
|
if (CompositorService.isHyprland)
|
||||||
updateHyprlandLayout();
|
updateHyprlandLayout();
|
||||||
|
|
||||||
|
fprintdDeviceProbe.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLastSuccessfulUser() {
|
function applyLastSuccessfulUser() {
|
||||||
|
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||||
|
return;
|
||||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||||
GreeterState.username = lastUser;
|
GreeterState.username = lastUser;
|
||||||
GreeterState.usernameInput = lastUser;
|
GreeterState.usernameInput = lastUser;
|
||||||
GreeterState.showPasswordInput = true;
|
GreeterState.showPasswordInput = true;
|
||||||
PortalService.getGreeterUserProfileImage(lastUser);
|
PortalService.getGreeterUserProfileImage(lastUser);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitUsername(rawValue) {
|
||||||
|
const user = (rawValue || "").trim();
|
||||||
|
if (!user)
|
||||||
|
return;
|
||||||
|
if (GreeterState.username !== user) {
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
|
externalAuthAutoStartedForUser = "";
|
||||||
|
}
|
||||||
|
GreeterState.username = user;
|
||||||
|
GreeterState.showPasswordInput = true;
|
||||||
|
PortalService.getGreeterUserProfileImage(user);
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitBufferedPassword() {
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
|
||||||
|
Greetd.respond(GreeterState.passwordBuffer || "");
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPasswordSessionTransition() {
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (!passwordSubmitRequested && !hasPasswordBuffer)
|
||||||
|
return;
|
||||||
|
if (cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelingExternalAuthForPassword = true;
|
||||||
|
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAuthSession(submitPassword) {
|
||||||
|
submitPassword = submitPassword === true;
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking)
|
||||||
|
return;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (Greetd.state !== GreetdState.Inactive) {
|
||||||
|
if (pendingPasswordResponse && submitPassword)
|
||||||
|
submitBufferedPassword();
|
||||||
|
else if (submitPassword)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cancelingExternalAuthForPassword) {
|
||||||
|
if (submitPassword)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = submitPassword;
|
||||||
|
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
|
||||||
|
// Use greeterExternalAuthAvailable so systems with pam_fprintd but no hardware don't incur the 30 s wait.
|
||||||
|
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterExternalAuthAvailable;
|
||||||
|
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.createSession(GreeterState.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeAutoStartExternalAuth() {
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (!root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
|
||||||
|
return;
|
||||||
|
if (externalAuthAutoStartedForUser === GreeterState.username)
|
||||||
|
return;
|
||||||
|
|
||||||
|
externalAuthAutoStartedForUser = GreeterState.username;
|
||||||
|
startAuthSession(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalAuthPrompt(message, responseRequired) {
|
||||||
|
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
|
||||||
|
return !responseRequired;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
WeatherService.removeRef();
|
WeatherService.removeRef();
|
||||||
@@ -113,6 +603,34 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives.
|
||||||
|
Process {
|
||||||
|
id: fprintdDeviceProbe
|
||||||
|
running: false
|
||||||
|
// sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
|
||||||
|
// keeping the PAM-only fallback active in those cases.
|
||||||
|
command: ["sh", "-c",
|
||||||
|
"command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
|
||||||
|
"gdbus call --system " +
|
||||||
|
"--dest net.reactivated.Fprint " +
|
||||||
|
"--object-path /net/reactivated/Fprint/Manager " +
|
||||||
|
"--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
|
||||||
|
"|| echo PROBE_UNAVAILABLE"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.includes("PROBE_UNAVAILABLE"))
|
||||||
|
return; // PAM-only fallback stays active
|
||||||
|
root.fprintdHasDevice = text.includes("objectpath");
|
||||||
|
root.fprintdProbeComplete = true;
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: function(exitCode, exitStatus) {
|
||||||
|
if (!root.fprintdProbeComplete)
|
||||||
|
root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CompositorService.isHyprland ? Hyprland : null
|
target: CompositorService.isHyprland ? Hyprland : null
|
||||||
enabled: CompositorService.isHyprland
|
enabled: CompositorService.isHyprland
|
||||||
@@ -143,10 +661,39 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greeterWallpaperOverrideFile
|
||||||
|
path: GreetdSettings.greeterWallpaperOverridePath
|
||||||
|
printErrors: false
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: root.greeterWallpaperOverrideExists = true
|
||||||
|
onLoadFailed: root.greeterWallpaperOverrideExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GreetdSettings
|
||||||
|
function onGreeterWallpaperOverridePathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperOverridePath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
function onGreeterWallpaperPathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperPath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankBackdrop {
|
DankBackdrop {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
visible: {
|
visible: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return false;
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
@@ -159,12 +706,14 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
||||||
}
|
}
|
||||||
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
|
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
cache: true
|
cache: true
|
||||||
@@ -327,10 +876,7 @@ Item {
|
|||||||
anchors.top: clockContainer.bottom
|
anchors.top: clockContainer.bottom
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
text: {
|
text: {
|
||||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
|
|
||||||
}
|
|
||||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -399,6 +945,9 @@ Item {
|
|||||||
if (GreeterState.showPasswordInput && revealButton.visible) {
|
if (GreeterState.showPasswordInput && revealButton.visible) {
|
||||||
margin += revealButton.width;
|
margin += revealButton.width;
|
||||||
}
|
}
|
||||||
|
if (externalAuthButton.visible) {
|
||||||
|
margin += externalAuthButton.width;
|
||||||
|
}
|
||||||
if (virtualKeyboardButton.visible) {
|
if (virtualKeyboardButton.visible) {
|
||||||
margin += virtualKeyboardButton.width;
|
margin += virtualKeyboardButton.width;
|
||||||
}
|
}
|
||||||
@@ -415,21 +964,18 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
GreeterState.passwordBuffer = text;
|
GreeterState.passwordBuffer = text;
|
||||||
|
if (!text || text.length === 0)
|
||||||
|
root.passwordSubmitRequested = false;
|
||||||
} else {
|
} else {
|
||||||
GreeterState.usernameInput = text;
|
GreeterState.usernameInput = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
|
root.startAuthSession(true);
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
GreeterState.username = text.trim();
|
root.submitUsername(text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
syncingFromState = true;
|
syncingFromState = true;
|
||||||
text = "";
|
text = "";
|
||||||
syncingFromState = false;
|
syncingFromState = false;
|
||||||
@@ -461,14 +1007,14 @@ Item {
|
|||||||
|
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
if (GreeterState.unlocking) {
|
if (GreeterState.unlocking) {
|
||||||
return "Logging in...";
|
return "Logging in...";
|
||||||
}
|
}
|
||||||
if (Greetd.state !== GreetdState.Inactive) {
|
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
|
||||||
return "Authenticating...";
|
return "Authenticating...";
|
||||||
}
|
}
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
@@ -476,7 +1022,7 @@ Item {
|
|||||||
}
|
}
|
||||||
return "Username...";
|
return "Username...";
|
||||||
}
|
}
|
||||||
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
|
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||||
|
|
||||||
@@ -498,7 +1044,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
@@ -528,15 +1074,27 @@ Item {
|
|||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: revealButton
|
id: revealButton
|
||||||
|
|
||||||
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
|
||||||
anchors.rightMargin: 0
|
anchors.rightMargin: 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: parent.showPassword = !parent.showPassword
|
onClicked: parent.showPassword = !parent.showPassword
|
||||||
}
|
}
|
||||||
|
DankActionButton {
|
||||||
|
id: externalAuthButton
|
||||||
|
|
||||||
|
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
|
||||||
|
buttonSize: 32
|
||||||
|
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
|
enabled: visible
|
||||||
|
onClicked: root.startAuthSession(false)
|
||||||
|
}
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: virtualKeyboardButton
|
id: virtualKeyboardButton
|
||||||
|
|
||||||
@@ -545,7 +1103,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (keyboard_controller.isKeyboardActive) {
|
if (keyboard_controller.isKeyboardActive) {
|
||||||
@@ -564,19 +1122,14 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard_return"
|
iconName: "keyboard_return"
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: true
|
enabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (GreeterState.username) {
|
root.startAuthSession(true);
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (inputField.text.trim()) {
|
if (inputField.text.trim()) {
|
||||||
GreeterState.username = inputField.text.trim();
|
root.submitUsername(inputField.text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,20 +1154,16 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 20
|
Layout.preferredHeight: 38
|
||||||
Layout.topMargin: -Theme.spacingS
|
Layout.topMargin: -Theme.spacingS
|
||||||
Layout.bottomMargin: -Theme.spacingS
|
Layout.bottomMargin: -Theme.spacingS
|
||||||
text: {
|
text: root.authFeedbackMessage
|
||||||
if (GreeterState.pamState === "error")
|
|
||||||
return "Authentication error - try again";
|
|
||||||
if (GreeterState.pamState === "fail")
|
|
||||||
return "Incorrect password";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
opacity: GreeterState.pamState !== "" ? 1 : 0
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
opacity: root.authFeedbackMessage !== "" ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -667,6 +1216,7 @@ Item {
|
|||||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||||
onClicked: {
|
onClicked: {
|
||||||
GreeterState.reset();
|
GreeterState.reset();
|
||||||
|
root.externalAuthAutoStartedForUser = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
PortalService.profileImage = "";
|
PortalService.profileImage = "";
|
||||||
}
|
}
|
||||||
@@ -1029,9 +1579,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (!GreetdMemory.memoryReady)
|
if (!GreetdMemory.memoryReady)
|
||||||
return;
|
return;
|
||||||
|
if (!GreetdSettings.settingsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
const savedSession = GreetdMemory.lastSessionId;
|
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
|
||||||
if (savedSession) {
|
if (savedSession && GreetdSettings.rememberLastSession) {
|
||||||
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
||||||
if (GreeterState.sessionPaths[i] === savedSession) {
|
if (GreeterState.sessionPaths[i] === savedSession) {
|
||||||
GreeterState.currentSessionIndex = i;
|
GreeterState.currentSessionIndex = i;
|
||||||
@@ -1164,44 +1716,151 @@ Item {
|
|||||||
|
|
||||||
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
||||||
if (responseRequired) {
|
if (responseRequired) {
|
||||||
Greetd.respond(GreeterState.passwordBuffer);
|
cancelingExternalAuthForPassword = false;
|
||||||
GreeterState.passwordBuffer = "";
|
passwordSessionTransitionRetryCount = 0;
|
||||||
inputField.text = "";
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = true;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (!passwordSubmitRequested && hasPasswordBuffer)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
if (passwordSubmitRequested && !root.submitBufferedPassword())
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
if (passwordSubmitRequested || hasPasswordBuffer) {
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
} else {
|
||||||
|
authTimeout.stop();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!error)
|
pendingPasswordResponse = false;
|
||||||
|
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
|
||||||
|
if (!passwordSubmitRequested)
|
||||||
|
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
|
||||||
|
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
|
||||||
|
authTimeout.interval = externalAuthTimeoutMs;
|
||||||
|
else
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
Greetd.respond("");
|
Greetd.respond("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onStateChanged() {
|
||||||
|
if (Greetd.state === GreetdState.Inactive) {
|
||||||
|
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
if (resumePasswordSubmit) {
|
||||||
|
Qt.callLater(function() {
|
||||||
|
root.startAuthSession(true);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onReadyToLaunch() {
|
function onReadyToLaunch() {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
||||||
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
||||||
if (!sessionCmd) {
|
if (!sessionCmd) {
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterState.unlocking = true;
|
GreeterState.unlocking = true;
|
||||||
launchTimeout.restart();
|
launchTimeout.restart();
|
||||||
|
if (GreetdSettings.rememberLastSession) {
|
||||||
GreetdMemory.setLastSessionId(sessionPath);
|
GreetdMemory.setLastSessionId(sessionPath);
|
||||||
|
} else if (GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
if (GreetdSettings.rememberLastUser) {
|
||||||
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
||||||
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
pendingLaunchCommand = sessionCmd;
|
||||||
|
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
||||||
|
memoryFlushTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuthFailure(message) {
|
function onAuthFailure(message) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
|
if (isLikelyLockoutMessage(message)) {
|
||||||
|
GreeterState.pamState = "max";
|
||||||
|
} else {
|
||||||
GreeterState.pamState = "fail";
|
GreeterState.pamState = "fail";
|
||||||
|
passwordFailureCount = passwordFailureCount + 1;
|
||||||
|
}
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: memoryFlushTimer
|
||||||
|
interval: memoryFlushDelayMs
|
||||||
|
onTriggered: {
|
||||||
|
if (!pendingLaunchCommand)
|
||||||
|
return;
|
||||||
|
const sessionCommand = pendingLaunchCommand;
|
||||||
|
const launchEnv = pendingLaunchEnv;
|
||||||
|
pendingLaunchCommand = "";
|
||||||
|
pendingLaunchEnv = [];
|
||||||
|
Greetd.launch(sessionCommand.split(" "), launchEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: authTimeout
|
||||||
|
interval: defaultAuthTimeoutMs
|
||||||
|
onTriggered: {
|
||||||
|
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
@@ -1215,8 +1874,11 @@ Item {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!GreeterState.unlocking)
|
if (!GreeterState.unlocking)
|
||||||
return;
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
Greetd.cancelSession();
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
@@ -1225,7 +1887,7 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: placeholderDelay
|
id: placeholderDelay
|
||||||
interval: 4000
|
interval: 4000
|
||||||
onTriggered: GreeterState.pamState = ""
|
onTriggered: clearAuthFeedback()
|
||||||
}
|
}
|
||||||
|
|
||||||
LockPowerMenu {
|
LockPowerMenu {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
|
|||||||
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
||||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
||||||
- **Session Memory**: Remembers last selected session and user
|
- **Session Memory**: Remembers last selected session and user
|
||||||
|
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
|
|||||||
dms-greeter --command sway
|
dms-greeter --command sway
|
||||||
dms-greeter --command mangowc
|
dms-greeter --command mangowc
|
||||||
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
||||||
|
dms-greeter --command niri --remember-last-user false --remember-last-session false
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure greetd to use it in `/etc/greetd/config.toml`:
|
Configure greetd to use it in `/etc/greetd/config.toml`:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ COMPOSITOR=""
|
|||||||
COMPOSITOR_CONFIG=""
|
COMPOSITOR_CONFIG=""
|
||||||
DMS_PATH="dms-greeter"
|
DMS_PATH="dms-greeter"
|
||||||
CACHE_DIR="/var/cache/dms-greeter"
|
CACHE_DIR="/var/cache/dms-greeter"
|
||||||
|
REMEMBER_LAST_SESSION=""
|
||||||
|
REMEMBER_LAST_USER=""
|
||||||
|
DEBUG_MODE=0
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -22,6 +25,15 @@ Options:
|
|||||||
(default: dms-greeter)
|
(default: dms-greeter)
|
||||||
--cache-dir PATH Cache directory for greeter data
|
--cache-dir PATH Cache directory for greeter data
|
||||||
(default: /var/cache/dms-greeter)
|
(default: /var/cache/dms-greeter)
|
||||||
|
--remember-last-session BOOL
|
||||||
|
Persist selected session to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--remember-last-user BOOL
|
||||||
|
Persist last successful username to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--no-save-session Alias for --remember-last-session false
|
||||||
|
--no-save-username Alias for --remember-last-user false
|
||||||
|
--debug Enable verbose startup logging to stderr
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -30,6 +42,7 @@ Examples:
|
|||||||
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
||||||
|
dms-greeter --command niri --no-save-session --no-save-username
|
||||||
dms-greeter --command mango
|
dms-greeter --command mango
|
||||||
dms-greeter --command labwc
|
dms-greeter --command labwc
|
||||||
EOF
|
EOF
|
||||||
@@ -43,6 +56,41 @@ require_command() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalize_bool_flag() {
|
||||||
|
local flag_name="$1"
|
||||||
|
local value="$2"
|
||||||
|
local normalized="${value,,}"
|
||||||
|
|
||||||
|
case "$normalized" in
|
||||||
|
1|true|yes|on)
|
||||||
|
echo "1"
|
||||||
|
;;
|
||||||
|
0|false|no|off)
|
||||||
|
echo "0"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_compositor() {
|
||||||
|
local log_tag="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemd-cat >/dev/null 2>&1; then
|
||||||
|
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local log_file="$CACHE_DIR/$log_tag.log"
|
||||||
|
exec "$@" >> "$log_file" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--command)
|
--command)
|
||||||
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
|
|||||||
CACHE_DIR="$2"
|
CACHE_DIR="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--remember-last-session)
|
||||||
|
REMEMBER_LAST_SESSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--remember-last-user)
|
||||||
|
REMEMBER_LAST_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-save-session)
|
||||||
|
REMEMBER_LAST_SESSION="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-save-username)
|
||||||
|
REMEMBER_LAST_USER="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug)
|
||||||
|
DEBUG_MODE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
@@ -111,9 +179,71 @@ export QT_QPA_PLATFORM=wayland
|
|||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||||
export EGL_PLATFORM=gbm
|
export EGL_PLATFORM=gbm
|
||||||
export DMS_RUN_GREETER=1
|
export DMS_RUN_GREETER=1
|
||||||
|
|
||||||
|
ensure_cache_tree() {
|
||||||
|
local base="$1"
|
||||||
|
mkdir -p "$base/.local/state" "$base/.local/state/wireplumber" "$base/.local/share" "$base/.cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
|
||||||
|
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
|
||||||
|
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
|
||||||
|
CACHE_DIR="$FALLBACK_CACHE_DIR"
|
||||||
|
if ! ensure_cache_tree "$CACHE_DIR"; then
|
||||||
|
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_SESSION
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
|
||||||
|
DMS_SAVE_SESSION=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_SESSION=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_SESSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_USER
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
|
||||||
|
DMS_SAVE_USERNAME=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_USERNAME=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_USERNAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
export HOME="$CACHE_DIR"
|
||||||
|
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
||||||
|
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
||||||
|
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
|
||||||
|
|
||||||
|
# Propagate correct XDG dirs into the systemd user session so socket-activated
|
||||||
|
# services (e.g. wireplumber) don't inherit HOME=/ from /etc/passwd.
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl --user set-environment \
|
||||||
|
HOME="$CACHE_DIR" \
|
||||||
|
XDG_STATE_HOME="$CACHE_DIR/.local/state" \
|
||||||
|
XDG_DATA_HOME="$CACHE_DIR/.local/share" \
|
||||||
|
XDG_CACHE_HOME="$CACHE_DIR/.cache" 2>/dev/null || true
|
||||||
|
if systemctl --user is-active --quiet wireplumber.service 2>/dev/null; then
|
||||||
|
systemctl --user restart wireplumber.service 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep greeter VT clean by default; callers can override via env or --debug.
|
||||||
|
if [[ -z "${RUST_LOG:-}" ]]; then
|
||||||
|
export RUST_LOG=warn
|
||||||
|
fi
|
||||||
|
if [[ -z "${NIRI_LOG:-}" ]]; then
|
||||||
|
export NIRI_LOG=warn
|
||||||
|
fi
|
||||||
|
|
||||||
if command -v qs >/dev/null 2>&1; then
|
if command -v qs >/dev/null 2>&1; then
|
||||||
QS_BIN="qs"
|
QS_BIN="qs"
|
||||||
@@ -130,7 +260,9 @@ if [[ "$DMS_PATH" == /* ]]; then
|
|||||||
else
|
else
|
||||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
||||||
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
||||||
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
||||||
|
fi
|
||||||
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
||||||
else
|
else
|
||||||
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
||||||
@@ -151,6 +283,10 @@ hotkey-overlay {
|
|||||||
|
|
||||||
environment {
|
environment {
|
||||||
DMS_RUN_GREETER "1"
|
DMS_RUN_GREETER "1"
|
||||||
|
HOME "$CACHE_DIR"
|
||||||
|
XDG_STATE_HOME "$CACHE_DIR/.local/state"
|
||||||
|
XDG_DATA_HOME "$CACHE_DIR/.local/share"
|
||||||
|
XDG_CACHE_HOME "$CACHE_DIR/.cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
@@ -192,7 +328,7 @@ NIRI_EOF
|
|||||||
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
||||||
NIRI_EOF
|
NIRI_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
exec niri -c "$COMPOSITOR_CONFIG"
|
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
hyprland)
|
hyprland)
|
||||||
@@ -204,11 +340,16 @@ NIRI_EOF
|
|||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp)
|
||||||
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
|
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
|
||||||
env = DMS_RUN_GREETER,1
|
env = DMS_RUN_GREETER,1
|
||||||
|
env = HOME,$CACHE_DIR
|
||||||
|
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
|
||||||
|
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
|
||||||
|
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
|
||||||
|
|
||||||
misc {
|
misc {
|
||||||
disable_hyprland_logo = true
|
disable_hyprland_logo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
||||||
HYPRLAND_EOF
|
HYPRLAND_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
@@ -217,14 +358,19 @@ HYPRLAND_EOF
|
|||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
||||||
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
|
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
|
||||||
|
|
||||||
|
env = HOME,$CACHE_DIR
|
||||||
|
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
|
||||||
|
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
|
||||||
|
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
|
||||||
|
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
||||||
HYPRLAND_EOF
|
HYPRLAND_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
if command -v start-hyprland >/dev/null 2>&1; then
|
||||||
exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
||||||
else
|
else
|
||||||
exec Hyprland -c "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -233,6 +379,7 @@ HYPRLAND_EOF
|
|||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp)
|
||||||
cat > "$TEMP_CONFIG" << SWAY_EOF
|
cat > "$TEMP_CONFIG" << SWAY_EOF
|
||||||
|
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
|
||||||
exec "$QS_CMD; swaymsg exit"
|
exec "$QS_CMD; swaymsg exit"
|
||||||
SWAY_EOF
|
SWAY_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
@@ -241,11 +388,12 @@ SWAY_EOF
|
|||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
||||||
cat >> "$TEMP_CONFIG" << SWAY_EOF
|
cat >> "$TEMP_CONFIG" << SWAY_EOF
|
||||||
|
|
||||||
|
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
|
||||||
exec "$QS_CMD; swaymsg exit"
|
exec "$QS_CMD; swaymsg exit"
|
||||||
SWAY_EOF
|
SWAY_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
scroll)
|
scroll)
|
||||||
@@ -253,6 +401,7 @@ SWAY_EOF
|
|||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp)
|
||||||
cat > "$TEMP_CONFIG" << SCROLL_EOF
|
cat > "$TEMP_CONFIG" << SCROLL_EOF
|
||||||
|
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
|
||||||
exec "$QS_CMD; scrollmsg exit"
|
exec "$QS_CMD; scrollmsg exit"
|
||||||
SCROLL_EOF
|
SCROLL_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
@@ -261,11 +410,12 @@ SCROLL_EOF
|
|||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
||||||
cat >> "$TEMP_CONFIG" << SCROLL_EOF
|
cat >> "$TEMP_CONFIG" << SCROLL_EOF
|
||||||
|
|
||||||
|
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
|
||||||
exec "$QS_CMD; scrollmsg exit"
|
exec "$QS_CMD; scrollmsg exit"
|
||||||
SCROLL_EOF
|
SCROLL_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec scroll -c "$COMPOSITOR_CONFIG"
|
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
miracle|miracle-wm)
|
miracle|miracle-wm)
|
||||||
@@ -285,24 +435,24 @@ exec "$QS_CMD; miraclemsg exit"
|
|||||||
MIRACLE_EOF
|
MIRACLE_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec miracle-wm -c "$COMPOSITOR_CONFIG"
|
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
labwc)
|
labwc)
|
||||||
require_command "labwc"
|
require_command "labwc"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
||||||
else
|
else
|
||||||
exec labwc --session "$QS_CMD"
|
exec_compositor "labwc" labwc --session "$QS_CMD"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
mango|mangowc)
|
mango|mangowc)
|
||||||
require_command "mango"
|
require_command "mango"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
||||||
else
|
else
|
||||||
exec mango -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|||||||
@@ -755,7 +755,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -764,6 +764,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.unlocking) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
if (pam.u2fPending) {
|
if (pam.u2fPending) {
|
||||||
pam.cancelU2fPending();
|
pam.cancelU2fPending();
|
||||||
@@ -1017,7 +1022,7 @@ Item {
|
|||||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||||
enabled: !demoMode
|
enabled: !demoMode
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!demoMode && !pam.u2fPending) {
|
if (!demoMode && !root.unlocking && !pam.u2fPending) {
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1626,6 +1631,7 @@ Item {
|
|||||||
onStateChanged: {
|
onStateChanged: {
|
||||||
root.pamState = state;
|
root.pamState = state;
|
||||||
if (state !== "") {
|
if (state !== "") {
|
||||||
|
root.unlocking = false;
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
passwordField.text = "";
|
passwordField.text = "";
|
||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
@@ -1641,6 +1647,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: pam
|
||||||
|
|
||||||
|
function onUnlockInProgressChanged() {
|
||||||
|
if (!pam.unlockInProgress && root.unlocking)
|
||||||
|
root.unlocking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: pam
|
target: pam
|
||||||
property: "buffer"
|
property: "buffer"
|
||||||
|
|||||||
@@ -25,6 +25,29 @@ Scope {
|
|||||||
signal flashMsg
|
signal flashMsg
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
|
function resetAuthFlows(): void {
|
||||||
|
passwd.abort();
|
||||||
|
fprint.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
unlockInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recoverFromAuthStall(newState: string): void {
|
||||||
|
resetAuthFlows();
|
||||||
|
state = newState;
|
||||||
|
flashMsg();
|
||||||
|
stateReset.restart();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
function completeUnlock(): void {
|
function completeUnlock(): void {
|
||||||
if (!unlockInProgress) {
|
if (!unlockInProgress) {
|
||||||
unlockInProgress = true;
|
unlockInProgress = true;
|
||||||
@@ -36,6 +59,7 @@ Scope {
|
|||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
u2fPending = false;
|
u2fPending = false;
|
||||||
u2fState = "";
|
u2fState = "";
|
||||||
|
unlockRequestTimeout.restart();
|
||||||
unlockRequested();
|
unlockRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +90,13 @@ Scope {
|
|||||||
printErrors: false
|
printErrors: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: loginConfigWatcher
|
||||||
|
|
||||||
|
path: "/etc/pam.d/login"
|
||||||
|
printErrors: false
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: u2fConfigWatcher
|
id: u2fConfigWatcher
|
||||||
|
|
||||||
@@ -77,7 +108,7 @@ Scope {
|
|||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
||||||
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
onMessageChanged: {
|
onMessageChanged: {
|
||||||
if (message.startsWith("The account is locked"))
|
if (message.startsWith("The account is locked"))
|
||||||
@@ -102,6 +133,13 @@ Scope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
root.unlockInProgress = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2f.abort();
|
||||||
|
|
||||||
if (res === PamResult.Error)
|
if (res === PamResult.Error)
|
||||||
root.state = "error";
|
root.state = "error";
|
||||||
else if (res === PamResult.MaxTries)
|
else if (res === PamResult.MaxTries)
|
||||||
@@ -114,10 +152,22 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: passwd
|
||||||
|
|
||||||
|
function onActiveChanged() {
|
||||||
|
if (passwd.active) {
|
||||||
|
passwdActiveTimeout.restart();
|
||||||
|
} else {
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PamContext {
|
PamContext {
|
||||||
id: fprint
|
id: fprint
|
||||||
|
|
||||||
property bool available
|
property bool available: SettingsData.lockFingerprintReady
|
||||||
property int tries
|
property int tries
|
||||||
property int errorTries
|
property int errorTries
|
||||||
|
|
||||||
@@ -173,7 +223,7 @@ Scope {
|
|||||||
PamContext {
|
PamContext {
|
||||||
id: u2f
|
id: u2f
|
||||||
|
|
||||||
property bool available
|
property bool available: SettingsData.lockU2fReady
|
||||||
|
|
||||||
function checkAvail(): void {
|
function checkAvail(): void {
|
||||||
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
|
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
|
||||||
@@ -202,7 +252,7 @@ Scope {
|
|||||||
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
onMessageChanged: {
|
onMessageChanged: {
|
||||||
if (message.toLowerCase().includes("touch"))
|
if (message !== "")
|
||||||
root.u2fState = "waiting";
|
root.u2fState = "waiting";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,26 +288,6 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: availProc
|
|
||||||
|
|
||||||
command: ["sh", "-c", "fprintd-list $USER"]
|
|
||||||
onExited: code => {
|
|
||||||
fprint.available = code === 0;
|
|
||||||
fprint.checkAvail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: u2fAvailProc
|
|
||||||
|
|
||||||
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
|
||||||
onExited: code => {
|
|
||||||
u2f.available = code === 0;
|
|
||||||
u2f.checkAvail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: errorRetry
|
id: errorRetry
|
||||||
|
|
||||||
@@ -279,6 +309,26 @@ Scope {
|
|||||||
onTriggered: root.cancelU2fPending()
|
onTriggered: root.cancelU2fPending()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: passwdActiveTimeout
|
||||||
|
|
||||||
|
interval: 15000
|
||||||
|
onTriggered: {
|
||||||
|
if (passwd.active)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: unlockRequestTimeout
|
||||||
|
|
||||||
|
interval: 8000
|
||||||
|
onTriggered: {
|
||||||
|
if (root.unlockInProgress)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: stateReset
|
id: stateReset
|
||||||
|
|
||||||
@@ -301,24 +351,17 @@ Scope {
|
|||||||
|
|
||||||
onLockSecuredChanged: {
|
onLockSecuredChanged: {
|
||||||
if (lockSecured) {
|
if (lockSecured) {
|
||||||
availProc.running = true;
|
SettingsData.refreshAuthAvailability();
|
||||||
u2fAvailProc.running = true;
|
|
||||||
root.state = "";
|
root.state = "";
|
||||||
root.fprintState = "";
|
root.fprintState = "";
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.unlockInProgress = false;
|
root.resetAuthFlows();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
} else {
|
} else {
|
||||||
fprint.abort();
|
root.resetAuthFlows();
|
||||||
passwd.abort();
|
|
||||||
u2f.abort();
|
|
||||||
errorRetry.running = false;
|
|
||||||
u2fErrorRetry.running = false;
|
|
||||||
u2fPendingTimeout.running = false;
|
|
||||||
root.u2fPending = false;
|
|
||||||
root.u2fState = "";
|
|
||||||
root.unlockInProgress = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,15 +372,24 @@ Scope {
|
|||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLockFingerprintReadyChanged(): void {
|
||||||
|
fprint.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
function onEnableU2fChanged(): void {
|
function onEnableU2fChanged(): void {
|
||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLockU2fReadyChanged(): void {
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
function onU2fModeChanged(): void {
|
function onU2fModeChanged(): void {
|
||||||
if (root.lockSecured) {
|
if (root.lockSecured) {
|
||||||
u2f.abort();
|
u2f.abort();
|
||||||
u2fErrorRetry.running = false;
|
u2fErrorRetry.running = false;
|
||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ DankPopout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:notification-center-popout"
|
layerNamespace: "dms:notification-center-popout"
|
||||||
fullHeightSurface: false
|
fullHeightSurface: true
|
||||||
|
|
||||||
property bool notificationHistoryVisible: false
|
property bool notificationHistoryVisible: false
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ QtObject {
|
|||||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _isFocusedScreen() {
|
||||||
|
if (!SettingsData.notificationFocusedMonitor)
|
||||||
|
return true;
|
||||||
|
const focused = CompositorService.getFocusedScreen();
|
||||||
|
return focused && manager.modelData && focused.name === manager.modelData.name;
|
||||||
|
}
|
||||||
|
|
||||||
function _sync(newWrappers) {
|
function _sync(newWrappers) {
|
||||||
for (const p of popupWindows.slice()) {
|
for (const p of popupWindows.slice()) {
|
||||||
if (!_isValidWindow(p) || p.exiting)
|
if (!_isValidWindow(p) || p.exiting)
|
||||||
@@ -118,7 +125,7 @@ QtObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const w of newWrappers) {
|
for (const w of newWrappers) {
|
||||||
if (w && !_hasWindowFor(w))
|
if (w && !_hasWindowFor(w) && _isFocusedScreen())
|
||||||
_insertAtTop(w);
|
_insertAtTop(w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -909,6 +909,9 @@ Singleton {
|
|||||||
case "dwl":
|
case "dwl":
|
||||||
DwlService.generateOutputsConfig(outputsData);
|
DwlService.generateOutputsConfig(outputsData);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
WlrOutputService.applyOutputsConfig(outputsData, outputs);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -417,6 +417,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Focused monitor only")
|
||||||
|
description: I18n.tr("Show notifications only on the currently focused monitor")
|
||||||
|
visible: parent.componentId === "notifications"
|
||||||
|
checked: SettingsData.notificationFocusedMonitor
|
||||||
|
onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked)
|
||||||
|
}
|
||||||
|
|
||||||
DankToggle {
|
DankToggle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: I18n.tr("Show on Last Display")
|
text: I18n.tr("Show on Last Display")
|
||||||
|
|||||||
@@ -160,6 +160,16 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("dockGroupByApp", checked)
|
onToggled: checked => SettingsData.set("dockGroupByApp", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "dockRestoreSpecialWorkspaceOnClick"
|
||||||
|
tags: ["dock", "hyprland", "special", "workspace", "restore"]
|
||||||
|
text: I18n.tr("Restore Special Workspace Windows")
|
||||||
|
description: I18n.tr("When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window")
|
||||||
|
checked: SettingsData.dockRestoreSpecialWorkspaceOnClick
|
||||||
|
visible: CompositorService.isHyprland
|
||||||
|
onToggled: checked => SettingsData.set("dockRestoreSpecialWorkspaceOnClick", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
settingKey: "dockIndicatorStyle"
|
settingKey: "dockIndicatorStyle"
|
||||||
tags: ["dock", "indicator", "style", "circle", "line"]
|
tags: ["dock", "indicator", "style", "circle", "line"]
|
||||||
|
|||||||
@@ -0,0 +1,784 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool greeterFprintToggleAvailable: SettingsData.greeterFingerprintCanEnable || SettingsData.greeterEnableFprint
|
||||||
|
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
|
||||||
|
|
||||||
|
function greeterFingerprintDescription() {
|
||||||
|
const source = SettingsData.greeterFingerprintSource;
|
||||||
|
const reason = SettingsData.greeterFingerprintReason;
|
||||||
|
|
||||||
|
if (source === "pam") {
|
||||||
|
switch (reason) {
|
||||||
|
case "configured_externally":
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.");
|
||||||
|
case "missing_enrollment":
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet.");
|
||||||
|
case "missing_reader":
|
||||||
|
return I18n.tr("PAM provides fingerprint auth, but no reader was detected.");
|
||||||
|
default:
|
||||||
|
return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case "ready":
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.");
|
||||||
|
case "missing_enrollment":
|
||||||
|
if (SettingsData.greeterEnableFprint)
|
||||||
|
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.");
|
||||||
|
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.");
|
||||||
|
case "missing_reader":
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
|
||||||
|
case "missing_pam_support":
|
||||||
|
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.");
|
||||||
|
default:
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function greeterU2fDescription() {
|
||||||
|
const source = SettingsData.greeterU2fSource;
|
||||||
|
const reason = SettingsData.greeterU2fReason;
|
||||||
|
|
||||||
|
if (source === "pam") {
|
||||||
|
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login.");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case "ready":
|
||||||
|
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Available.");
|
||||||
|
case "missing_key_registration":
|
||||||
|
if (SettingsData.greeterEnableU2f)
|
||||||
|
return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync.");
|
||||||
|
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
|
||||||
|
case "missing_pam_support":
|
||||||
|
return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.");
|
||||||
|
default:
|
||||||
|
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuthDetection() {
|
||||||
|
SettingsData.refreshAuthAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
refreshAuthDetection();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: greeterActionConfirm
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: greeterWallpaperBrowserModal
|
||||||
|
browserTitle: I18n.tr("Select greeter background image")
|
||||||
|
browserIcon: "wallpaper"
|
||||||
|
browserType: "wallpaper"
|
||||||
|
showHiddenFiles: true
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"]
|
||||||
|
onFileSelected: path => {
|
||||||
|
SettingsData.set("greeterWallpaperPath", path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property string greeterStatusText: ""
|
||||||
|
property bool greeterStatusRunning: false
|
||||||
|
property bool greeterSyncRunning: false
|
||||||
|
property bool greeterInstallActionRunning: false
|
||||||
|
property string greeterStatusStdout: ""
|
||||||
|
property string greeterStatusStderr: ""
|
||||||
|
property string greeterSyncStdout: ""
|
||||||
|
property string greeterSyncStderr: ""
|
||||||
|
property string greeterSudoProbeStderr: ""
|
||||||
|
property string greeterTerminalFallbackStderr: ""
|
||||||
|
property bool greeterTerminalFallbackFromPrecheck: false
|
||||||
|
property var cachedFontFamilies: []
|
||||||
|
property bool fontsEnumerated: false
|
||||||
|
property bool greeterBinaryExists: false
|
||||||
|
property bool greeterEnabled: false
|
||||||
|
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
|
||||||
|
|
||||||
|
readonly property string greeterActionLabel: {
|
||||||
|
if (!root.greeterInstalled)
|
||||||
|
return I18n.tr("Install");
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return I18n.tr("Activate");
|
||||||
|
return I18n.tr("Uninstall");
|
||||||
|
}
|
||||||
|
readonly property string greeterActionIcon: {
|
||||||
|
if (!root.greeterInstalled)
|
||||||
|
return "download";
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return "login";
|
||||||
|
return "delete";
|
||||||
|
}
|
||||||
|
readonly property var greeterActionCommand: {
|
||||||
|
if (!root.greeterInstalled)
|
||||||
|
return ["dms", "greeter", "install", "--terminal"];
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return ["dms", "greeter", "enable", "--terminal"];
|
||||||
|
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
|
||||||
|
}
|
||||||
|
property string greeterPendingAction: ""
|
||||||
|
|
||||||
|
function checkGreeterInstallState() {
|
||||||
|
greetdEnabledCheckProcess.running = true;
|
||||||
|
greeterBinaryCheckProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterStatus() {
|
||||||
|
greeterStatusText = "";
|
||||||
|
greeterStatusStdout = "";
|
||||||
|
greeterStatusStderr = "";
|
||||||
|
greeterStatusRunning = true;
|
||||||
|
greeterStatusProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterInstallAction() {
|
||||||
|
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
|
||||||
|
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
|
||||||
|
greeterInstallActionRunning = true;
|
||||||
|
greeterInstallActionProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptGreeterActionConfirm() {
|
||||||
|
var title, message, confirmText;
|
||||||
|
if (!root.greeterInstalled) {
|
||||||
|
title = I18n.tr("Install Greeter", "greeter action confirmation");
|
||||||
|
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
|
||||||
|
confirmText = I18n.tr("Install");
|
||||||
|
} else if (!root.greeterEnabled) {
|
||||||
|
title = I18n.tr("Activate Greeter", "greeter action confirmation");
|
||||||
|
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
|
||||||
|
confirmText = I18n.tr("Activate");
|
||||||
|
} else {
|
||||||
|
title = I18n.tr("Uninstall Greeter", "greeter action confirmation");
|
||||||
|
message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication.");
|
||||||
|
confirmText = I18n.tr("Uninstall");
|
||||||
|
}
|
||||||
|
greeterActionConfirm.showWithOptions({
|
||||||
|
"title": title,
|
||||||
|
"message": message,
|
||||||
|
"confirmText": confirmText,
|
||||||
|
"cancelText": I18n.tr("Cancel"),
|
||||||
|
"confirmColor": Theme.primary,
|
||||||
|
"onConfirm": () => root.runGreeterInstallAction(),
|
||||||
|
"onCancel": () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterSync() {
|
||||||
|
greeterSyncStdout = "";
|
||||||
|
greeterSyncStderr = "";
|
||||||
|
greeterSudoProbeStderr = "";
|
||||||
|
greeterTerminalFallbackStderr = "";
|
||||||
|
greeterTerminalFallbackFromPrecheck = false;
|
||||||
|
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…");
|
||||||
|
greeterSyncRunning = true;
|
||||||
|
greeterSudoProbeProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchGreeterSyncTerminalFallback(fromPrecheck, statusText) {
|
||||||
|
greeterTerminalFallbackFromPrecheck = fromPrecheck;
|
||||||
|
if (statusText && statusText !== "")
|
||||||
|
greeterStatusText = statusText;
|
||||||
|
greeterTerminalFallbackStderr = "";
|
||||||
|
greeterTerminalFallbackProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumerateFonts() {
|
||||||
|
if (fontsEnumerated)
|
||||||
|
return;
|
||||||
|
var fonts = [];
|
||||||
|
var availableFonts = Qt.fontFamilies();
|
||||||
|
for (var i = 0; i < availableFonts.length; i++) {
|
||||||
|
var fontName = availableFonts[i];
|
||||||
|
if (fontName.startsWith("."))
|
||||||
|
continue;
|
||||||
|
fonts.push(fontName);
|
||||||
|
}
|
||||||
|
fonts.sort();
|
||||||
|
fonts.unshift("Default");
|
||||||
|
cachedFontFamilies = fonts;
|
||||||
|
fontsEnumerated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
refreshAuthDetection();
|
||||||
|
Qt.callLater(enumerateFonts);
|
||||||
|
Qt.callLater(checkGreeterInstallState);
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greetdEnabledCheckProcess
|
||||||
|
command: ["systemctl", "is-enabled", "greetd"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterEnabled = text.trim() === "enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterBinaryCheckProcess
|
||||||
|
command: ["sh", "-c", "test -f /usr/bin/dms-greeter || test -f /usr/local/bin/dms-greeter"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterBinaryExists = (exitCode === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterStatusProcess
|
||||||
|
command: ["dms", "greeter", "status"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.greeterStatusStdout = text || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterStatusStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterStatusRunning = false;
|
||||||
|
const out = (root.greeterStatusStdout || "").trim();
|
||||||
|
const err = (root.greeterStatusStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.greeterStatusText = out !== "" ? out : I18n.tr("No status output.");
|
||||||
|
if (err !== "")
|
||||||
|
root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var failure = I18n.tr("Failed to run 'dms greeter status'. Ensure DMS is installed and dms is in PATH.", "greeter status error") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterSyncProcess
|
||||||
|
command: ["dms", "greeter", "sync", "--yes"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSyncStdout = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSyncStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterSyncRunning = false;
|
||||||
|
const out = (root.greeterSyncStdout || "").trim();
|
||||||
|
const err = (root.greeterSyncStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
var success = I18n.tr("Sync completed successfully.");
|
||||||
|
if (out !== "")
|
||||||
|
success = success + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
success = success + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = success;
|
||||||
|
} else {
|
||||||
|
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
root.launchGreeterSyncTerminalFallback(false, "");
|
||||||
|
}
|
||||||
|
root.checkGreeterInstallState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterSudoProbeProcess
|
||||||
|
command: ["sudo", "-n", "true"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSudoProbeStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
const err = (root.greeterSudoProbeStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.greeterStatusText = I18n.tr("Running greeter sync…");
|
||||||
|
greeterSyncProcess.running = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authNeeded = I18n.tr("Sync needs sudo authentication. Opening terminal so you can use password or fingerprint.");
|
||||||
|
if (err !== "")
|
||||||
|
authNeeded = authNeeded + "\n\n" + err;
|
||||||
|
root.launchGreeterSyncTerminalFallback(true, authNeeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterTerminalFallbackProcess
|
||||||
|
command: ["dms", "greeter", "sync", "--terminal", "--yes"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterTerminalFallbackStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterSyncRunning = false;
|
||||||
|
if (exitCode === 0) {
|
||||||
|
var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done.");
|
||||||
|
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")";
|
||||||
|
const err = (root.greeterTerminalFallbackStderr || "").trim();
|
||||||
|
if (err !== "")
|
||||||
|
fallback = fallback + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + fallback : fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterInstallActionProcess
|
||||||
|
command: root.greeterActionCommand
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterInstallActionRunning = false;
|
||||||
|
const pending = root.greeterPendingAction;
|
||||||
|
root.greeterPendingAction = "";
|
||||||
|
if (exitCode === 0) {
|
||||||
|
if (pending === "install")
|
||||||
|
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
|
||||||
|
else if (pending === "activate")
|
||||||
|
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
|
||||||
|
else
|
||||||
|
root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed.");
|
||||||
|
} else {
|
||||||
|
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
|
||||||
|
}
|
||||||
|
root.checkGreeterInstallState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var _lockDateFormatPresets: [
|
||||||
|
{
|
||||||
|
format: "",
|
||||||
|
label: I18n.tr("System Default", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd d",
|
||||||
|
label: I18n.tr("Day Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd MMM d",
|
||||||
|
label: I18n.tr("Day Month Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "MMM d",
|
||||||
|
label: I18n.tr("Month Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "M/d",
|
||||||
|
label: I18n.tr("Numeric (M/D)", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "d/M",
|
||||||
|
label: I18n.tr("Numeric (D/M)", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd d MMM yyyy",
|
||||||
|
label: I18n.tr("Full with Year", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "yyyy-MM-dd",
|
||||||
|
label: I18n.tr("ISO Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "dddd, MMMM d",
|
||||||
|
label: I18n.tr("Full Day & Month", "date format option")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingXL
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
topPadding: 4
|
||||||
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "info"
|
||||||
|
title: I18n.tr("Greeter Status")
|
||||||
|
settingKey: "greeterStatus"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(180, statusTextArea.implicitHeight + Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: statusTextArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
text: root.greeterStatusRunning ? I18n.tr("Checking…", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.family: "monospace"
|
||||||
|
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: root.greeterActionLabel
|
||||||
|
iconName: root.greeterActionIcon
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: root.promptGreeterActionConfirm()
|
||||||
|
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Refresh")
|
||||||
|
iconName: "refresh"
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: root.runGreeterStatus()
|
||||||
|
enabled: !root.greeterStatusRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Sync")
|
||||||
|
iconName: "sync"
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: root.runGreeterSync()
|
||||||
|
enabled: root.greeterInstalled && !root.greeterSyncRunning && !root.greeterInstallActionRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "fingerprint"
|
||||||
|
title: I18n.tr("Login Authentication")
|
||||||
|
settingKey: "greeterAuth"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterEnableFprint"
|
||||||
|
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
|
||||||
|
text: I18n.tr("Enable fingerprint at login")
|
||||||
|
description: root.greeterFingerprintDescription()
|
||||||
|
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.greeterEnableFprint
|
||||||
|
enabled: root.greeterFprintToggleAvailable
|
||||||
|
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterEnableU2f"
|
||||||
|
tags: ["greeter", "u2f", "security", "key", "login", "auth"]
|
||||||
|
text: I18n.tr("Enable security key at login")
|
||||||
|
description: root.greeterU2fDescription()
|
||||||
|
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.greeterEnableU2f
|
||||||
|
enabled: root.greeterU2fToggleAvailable
|
||||||
|
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "palette"
|
||||||
|
title: I18n.tr("Greeter Appearance")
|
||||||
|
settingKey: "greeterAppearance"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Font")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterFontFamily"
|
||||||
|
tags: ["greeter", "font", "typography"]
|
||||||
|
text: I18n.tr("Greeter font")
|
||||||
|
description: I18n.tr("Font used on the login screen")
|
||||||
|
options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"]
|
||||||
|
currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default")
|
||||||
|
enableFuzzySearch: true
|
||||||
|
popupWidthOffset: 100
|
||||||
|
maxPopupHeight: 400
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === "Default")
|
||||||
|
SettingsData.set("greeterFontFamily", "");
|
||||||
|
else
|
||||||
|
SettingsData.set("greeterFontFamily", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Time format")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterUse24Hour"
|
||||||
|
tags: ["greeter", "time", "24hour"]
|
||||||
|
text: I18n.tr("24-hour clock")
|
||||||
|
description: I18n.tr("Greeter only — does not affect main clock")
|
||||||
|
checked: SettingsData.greeterUse24HourClock
|
||||||
|
onToggled: checked => SettingsData.set("greeterUse24HourClock", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterShowSeconds"
|
||||||
|
tags: ["greeter", "time", "seconds"]
|
||||||
|
text: I18n.tr("Show seconds")
|
||||||
|
checked: SettingsData.greeterShowSeconds
|
||||||
|
onToggled: checked => SettingsData.set("greeterShowSeconds", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterPadHours"
|
||||||
|
tags: ["greeter", "time", "12hour"]
|
||||||
|
text: I18n.tr("Pad hours (02:00 vs 2:00)")
|
||||||
|
visible: !SettingsData.greeterUse24HourClock
|
||||||
|
checked: SettingsData.greeterPadHours12Hour
|
||||||
|
onToggled: checked => SettingsData.set("greeterPadHours12Hour", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Date format on greeter")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterLockDateFormat"
|
||||||
|
tags: ["greeter", "date", "format"]
|
||||||
|
text: I18n.tr("Date format")
|
||||||
|
description: I18n.tr("Greeter only — format for the date on the login screen")
|
||||||
|
options: root._lockDateFormatPresets.map(p => p.label)
|
||||||
|
currentValue: {
|
||||||
|
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || "";
|
||||||
|
var match = root._lockDateFormatPresets.find(p => p.format === current);
|
||||||
|
return match ? match.label : (current ? I18n.tr("Custom: ") + current : root._lockDateFormatPresets[0].label);
|
||||||
|
}
|
||||||
|
onValueChanged: value => {
|
||||||
|
var preset = root._lockDateFormatPresets.find(p => p.label === value);
|
||||||
|
SettingsData.set("greeterLockDateFormat", preset ? preset.format : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Background")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Use a custom image for the login screen, or leave empty to use your desktop wallpaper.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: greeterWallpaperPathField
|
||||||
|
width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS
|
||||||
|
placeholderText: I18n.tr("Use desktop wallpaper")
|
||||||
|
text: SettingsData.greeterWallpaperPath
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
onTextChanged: {
|
||||||
|
if (text !== SettingsData.greeterWallpaperPath)
|
||||||
|
SettingsData.set("greeterWallpaperPath", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: browseGreeterWallpaperButton
|
||||||
|
text: I18n.tr("Browse")
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: greeterWallpaperBrowserModal.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterWallpaperFillMode"
|
||||||
|
tags: ["greeter", "wallpaper", "background", "fill"]
|
||||||
|
text: I18n.tr("Wallpaper fill mode")
|
||||||
|
description: I18n.tr("How the background image is scaled")
|
||||||
|
options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode"))
|
||||||
|
currentValue: {
|
||||||
|
var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill");
|
||||||
|
var idx = root._wallpaperFillModes.indexOf(mode);
|
||||||
|
return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode");
|
||||||
|
}
|
||||||
|
onValueChanged: value => {
|
||||||
|
var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value);
|
||||||
|
if (idx >= 0)
|
||||||
|
SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "history"
|
||||||
|
title: I18n.tr("Greeter Behavior")
|
||||||
|
settingKey: "greeterBehavior"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Convenience options for the login screen. Sync to apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterRememberLastSession"
|
||||||
|
tags: ["greeter", "session", "remember", "login"]
|
||||||
|
text: I18n.tr("Remember last session")
|
||||||
|
description: I18n.tr("Pre-select the last used session on the greeter")
|
||||||
|
checked: SettingsData.greeterRememberLastSession
|
||||||
|
onToggled: checked => SettingsData.set("greeterRememberLastSession", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterRememberLastUser"
|
||||||
|
tags: ["greeter", "user", "remember", "login", "username"]
|
||||||
|
text: I18n.tr("Remember last user")
|
||||||
|
description: I18n.tr("Pre-fill the last successful username on the greeter")
|
||||||
|
checked: SettingsData.greeterRememberLastUser
|
||||||
|
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "extension"
|
||||||
|
title: I18n.tr("Dependencies & documentation")
|
||||||
|
settingKey: "greeterDeps"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Installation and PAM setup: see the ") + "<a href=\"https://danklinux.com/docs/dankgreeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">DankGreeter docs</a> " + I18n.tr("or run ") + "'dms greeter install'."
|
||||||
|
textFormat: Text.RichText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
linkColor: Theme.primary
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
onLinkActivated: url => Qt.openUrlExternally(url)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,51 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
|
||||||
|
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
|
||||||
|
|
||||||
|
function lockFingerprintDescription() {
|
||||||
|
switch (SettingsData.lockFingerprintReason) {
|
||||||
|
case "ready":
|
||||||
|
return I18n.tr("Use fingerprint authentication for the lock screen.");
|
||||||
|
case "missing_enrollment":
|
||||||
|
if (SettingsData.enableFprint)
|
||||||
|
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.");
|
||||||
|
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
|
||||||
|
case "missing_reader":
|
||||||
|
return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
|
||||||
|
case "missing_pam_support":
|
||||||
|
return I18n.tr("Not available — install fprintd and pam_fprintd.");
|
||||||
|
default:
|
||||||
|
return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockU2fDescription() {
|
||||||
|
switch (SettingsData.lockU2fReason) {
|
||||||
|
case "ready":
|
||||||
|
return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
|
||||||
|
case "missing_key_registration":
|
||||||
|
if (SettingsData.enableU2f)
|
||||||
|
return I18n.tr("Enabled, but no registered security key was found yet. Register a key or update your U2F config.");
|
||||||
|
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
|
||||||
|
case "missing_pam_support":
|
||||||
|
return I18n.tr("Not available — install or configure pam_u2f.");
|
||||||
|
default:
|
||||||
|
return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuthDetection() {
|
||||||
|
SettingsData.refreshAuthAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refreshAuthDetection()
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
refreshAuthDetection();
|
||||||
|
}
|
||||||
|
|
||||||
FileBrowserModal {
|
FileBrowserModal {
|
||||||
id: videoBrowserModal
|
id: videoBrowserModal
|
||||||
browserTitle: I18n.tr("Select Video or Folder")
|
browserTitle: I18n.tr("Select Video or Folder")
|
||||||
@@ -172,10 +217,10 @@ Item {
|
|||||||
settingKey: "enableFprint"
|
settingKey: "enableFprint"
|
||||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||||
text: I18n.tr("Enable fingerprint authentication")
|
text: I18n.tr("Enable fingerprint authentication")
|
||||||
description: SettingsData.fprintdAvailable ? I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)") : I18n.tr("Not enrolled", "fingerprint not detected status")
|
description: root.lockFingerprintDescription()
|
||||||
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.enableFprint
|
checked: SettingsData.enableFprint
|
||||||
enabled: SettingsData.fprintdAvailable
|
enabled: root.lockFprintToggleAvailable
|
||||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +228,10 @@ Item {
|
|||||||
settingKey: "enableU2f"
|
settingKey: "enableU2f"
|
||||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
||||||
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
||||||
description: SettingsData.u2fAvailable ? I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting") : I18n.tr("Not enrolled", "security key not detected status")
|
description: root.lockU2fDescription()
|
||||||
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.enableU2f
|
checked: SettingsData.enableU2f
|
||||||
enabled: SettingsData.u2fAvailable
|
enabled: root.lockU2fToggleAvailable
|
||||||
onToggled: checked => SettingsData.set("enableU2f", checked)
|
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +240,7 @@ Item {
|
|||||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||||
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
||||||
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||||
visible: SettingsData.u2fAvailable && SettingsData.enableU2f
|
visible: SettingsData.enableU2f
|
||||||
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
||||||
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
||||||
onValueChanged: value => {
|
onValueChanged: value => {
|
||||||
@@ -245,7 +290,7 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Path to a video file or folder containing videos")
|
text: I18n.tr("Path to a video file or folder containing videos")
|
||||||
font.pixelSize: Theme.fontSizeXSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.outlineVariant
|
color: Theme.outlineVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var muxTypeOptions: [
|
||||||
|
"tmux",
|
||||||
|
"zellij"
|
||||||
|
]
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingXL
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
topPadding: 4
|
||||||
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "multiplexer", "tmux", "zellij", "type"]
|
||||||
|
title: I18n.tr("Multiplexer")
|
||||||
|
iconName: "terminal"
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "multiplexer", "tmux", "zellij", "type", "backend"]
|
||||||
|
settingKey: "muxType"
|
||||||
|
text: I18n.tr("Multiplexer Type")
|
||||||
|
description: I18n.tr("Terminal multiplexer backend to use")
|
||||||
|
options: root.muxTypeOptions
|
||||||
|
currentValue: SettingsData.muxType
|
||||||
|
onValueChanged: value => SettingsData.set("muxType", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "terminal", "custom", "command", "script"]
|
||||||
|
title: I18n.tr("Terminal")
|
||||||
|
iconName: "desktop_windows"
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "custom", "command", "override"]
|
||||||
|
settingKey: "muxUseCustomCommand"
|
||||||
|
text: I18n.tr("Use Custom Command")
|
||||||
|
description: I18n.tr("Override terminal with a custom command or script")
|
||||||
|
checked: SettingsData.muxUseCustomCommand
|
||||||
|
onToggled: checked => SettingsData.set("muxUseCustomCommand", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent?.width ?? 0
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: SettingsData.muxUseCustomCommand
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("The custom command used when attaching to sessions (receives the session name as the first argument)")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
width: parent.width
|
||||||
|
text: SettingsData.muxCustomCommand
|
||||||
|
placeholderText: I18n.tr("Enter command or script path")
|
||||||
|
onTextEdited: SettingsData.set("muxCustomCommand", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "session", "filter", "exclude", "hide"]
|
||||||
|
title: I18n.tr("Session Filter")
|
||||||
|
iconName: "filter_list"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent?.width ?? 0
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
width: parent.width
|
||||||
|
text: SettingsData.muxSessionFilter
|
||||||
|
placeholderText: I18n.tr("e.g., scratch, /^tmp_.*/, build")
|
||||||
|
onTextEdited: SettingsData.set("muxSessionFilter", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user