mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 23:32:50 -04:00
Compare commits
2 Commits
f71d2d223e
..
anims
| Author | SHA1 | Date | |
|---|---|---|---|
| 62bf9c6efe | |||
| 61a77bd186 |
@@ -20,7 +20,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v6
|
# uses: actions/checkout@v4
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
# fetch-depth: 0
|
# fetch-depth: 0
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -192,12 +192,12 @@ jobs:
|
|||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -195,13 +195,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Wait before OBS upload
|
|
||||||
run: sleep 3
|
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
@@ -347,7 +344,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -157,12 +157,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
@@ -242,11 +242,7 @@ jobs:
|
|||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
# ppa-upload.sh uploads to questing + resolute when series is omitted
|
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||||
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
|
|
||||||
echo "::error::Upload failed for $PKG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||||
[ -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 ${{ github.ref_name }}
|
git pull --rebase origin master
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
else
|
else
|
||||||
echo "No changes to flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
# 1.5.0
|
|
||||||
- Overhauled shadows
|
|
||||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
|
||||||
- Greeter stuff
|
|
||||||
- Terminal mux
|
|
||||||
- Locale overrides
|
|
||||||
- new neovim theming
|
|
||||||
|
|
||||||
# 1.4.0
|
# 1.4.0
|
||||||
|
|
||||||
- Overhauled system monitor, graphs, styling
|
- Overhauled system monitor, graphs, styling
|
||||||
|
|||||||
+1
-3
@@ -86,9 +86,7 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
6. Make your changes, test, and open a pull request.
|
|
||||||
|
|
||||||
### I18n/Localization
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ clean:
|
|||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
lint-qml:
|
|
||||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -79,7 +76,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: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -133,7 +130,6 @@ help:
|
|||||||
@echo " all (default) - Build the DMS binary"
|
@echo " all (default) - Build the DMS binary"
|
||||||
@echo " build - Same as 'all'"
|
@echo " build - Same as 'all'"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
|||||||
+3
-3
@@ -63,19 +63,19 @@ endif
|
|||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
install:
|
install: build
|
||||||
@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:
|
install-all: build-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:
|
install-dankinstall: 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"
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var authCmd = &cobra.Command{
|
|
||||||
Use: "auth",
|
|
||||||
Short: "Manage DMS authentication sync",
|
|
||||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
|
||||||
}
|
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
|
||||||
Use: "sync",
|
|
||||||
Short: "Sync DMS authentication configuration",
|
|
||||||
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
|
||||||
if term {
|
|
||||||
if err := syncAuthInTerminal(yes); err != nil {
|
|
||||||
log.Fatalf("Error launching auth sync in terminal: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := syncAuth(yes); err != nil {
|
|
||||||
log.Fatalf("Error syncing authentication: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
|
||||||
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAuth(nonInteractive bool) error {
|
|
||||||
if !nonInteractive {
|
|
||||||
fmt.Println("=== DMS Authentication Sync ===")
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc := func(msg string) {
|
|
||||||
fmt.Println(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !nonInteractive {
|
|
||||||
fmt.Println("\n=== Authentication Sync Complete ===")
|
|
||||||
fmt.Println("\nAuthentication changes have been applied.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAuthInTerminal(nonInteractive bool) error {
|
|
||||||
syncFlags := make([]string, 0, 1)
|
|
||||||
if nonInteractive {
|
|
||||||
syncFlags = append(syncFlags, "--yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
shellSyncCmd := "dms auth sync"
|
|
||||||
if len(syncFlags) > 0 {
|
|
||||||
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
|
||||||
}
|
|
||||||
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
|
||||||
return runCommandInTerminal(shellCmd)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var blurCmd = &cobra.Command{
|
|
||||||
Use: "blur",
|
|
||||||
Short: "Background blur utilities",
|
|
||||||
}
|
|
||||||
|
|
||||||
var blurCheckCmd = &cobra.Command{
|
|
||||||
Use: "check",
|
|
||||||
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: runBlurCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
blurCmd.AddCommand(blurCheckCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runBlurCheck(cmd *cobra.Command, args []string) {
|
|
||||||
supported, err := blur.ProbeSupport()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch supported {
|
|
||||||
case true:
|
|
||||||
fmt.Println("supported")
|
|
||||||
default:
|
|
||||||
fmt.Println("unsupported")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -222,19 +222,16 @@ func init() {
|
|||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
copyFromStdin := false
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) > 0:
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
case clipCopyDownload || clipCopyType == "__multi__":
|
default:
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("read stdin: %v", err)
|
log.Fatalf("read stdin: %v", err)
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
copyFromStdin = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipCopyDownload {
|
if clipCopyDownload {
|
||||||
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if copyFromStdin {
|
|
||||||
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("copy: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ Output format flags (mutually exclusive, default: --hex):
|
|||||||
--cmyk - CMYK values (C% M% Y% K%)
|
--cmyk - CMYK values (C% M% Y% K%)
|
||||||
--json - JSON with all formats
|
--json - JSON with all formats
|
||||||
|
|
||||||
Optional:
|
|
||||||
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
dms color pick # Pick color, output as hex
|
dms color pick # Pick color, output as hex
|
||||||
dms color pick --rgb # Output as RGB
|
dms color pick --rgb # Output as RGB
|
||||||
@@ -56,7 +53,6 @@ func init() {
|
|||||||
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||||
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||||
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
|
|
||||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
@@ -117,15 +113,7 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
fmt.Println(output)
|
fmt.Println(output)
|
||||||
return
|
} else if color.IsDark() {
|
||||||
}
|
|
||||||
|
|
||||||
if raw, _ := cmd.Flags().GetBool("raw"); raw {
|
|
||||||
fmt.Printf("%s\n", output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if color.IsDark() {
|
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
|
PreRunE: findConfig,
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -525,6 +526,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
randrCmd,
|
||||||
blurCmd,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
if currentCategory != -1 {
|
if currentCategory != -1 {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
||||||
currentCategory = r.category
|
currentCategory = r.category
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
|
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
|
||||||
|
|
||||||
if doctorVerbose && r.details != "" {
|
if doctorVerbose && r.details != "" {
|
||||||
fmt.Fprintf(&sb, " - %s\n", r.details)
|
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("\n---\n")
|
sb.WriteString("\n---\n")
|
||||||
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
sb.WriteString(fmt.Sprintf("**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()
|
||||||
}
|
}
|
||||||
|
|||||||
+154
-1129
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
var calls []string
|
|
||||||
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
||||||
if dmsPath != "/tmp/dms" {
|
|
||||||
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
|
||||||
}
|
|
||||||
if compositor != "niri" {
|
|
||||||
t.Fatalf("unexpected compositor %q", compositor)
|
|
||||||
}
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
calls = append(calls, "configs")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotOptions sharedpam.SyncAuthOptions
|
|
||||||
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
gotOptions = options
|
|
||||||
calls = append(calls, "auth")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
|
||||||
ForceGreeterAuth: true,
|
|
||||||
}, func() {
|
|
||||||
calls = append(calls, "before-auth")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantCalls := []string{"configs", "before-auth", "auth"}
|
|
||||||
if !reflect.DeepEqual(calls, wantCalls) {
|
|
||||||
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
|
||||||
}
|
|
||||||
if !gotOptions.ForceGreeterAuth {
|
|
||||||
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
|
||||||
return errors.New("config sync failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
authCalled := false
|
|
||||||
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
|
||||||
authCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
|
||||||
if err == nil || err.Error() != "config sync failed" {
|
|
||||||
t.Fatalf("expected config sync error, got %v", err)
|
|
||||||
}
|
|
||||||
if authCalled {
|
|
||||||
t.Fatal("expected auth sync not to run after config sync failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,11 +57,10 @@ func init() {
|
|||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||||
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
@@ -78,7 +77,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -89,7 +87,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
Mode: matugen.ColorMode(mode),
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
Contrast: contrast,
|
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
@@ -131,7 +128,6 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
"skipTemplates": opts.SkipTemplates,
|
||||||
"contrast": opts.Contrast,
|
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ var (
|
|||||||
ssNoClipboard bool
|
ssNoClipboard bool
|
||||||
ssNoFile bool
|
ssNoFile bool
|
||||||
ssNoNotify bool
|
ssNoNotify bool
|
||||||
ssNoConfirm bool
|
|
||||||
ssReset bool
|
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,10 +50,8 @@ Examples:
|
|||||||
dms screenshot output -o DP-1 # Specific output
|
dms screenshot output -o DP-1 # Specific output
|
||||||
dms screenshot window # Focused window (Hyprland)
|
dms screenshot window # Focused window (Hyprland)
|
||||||
dms screenshot last # Last region (pre-selected)
|
dms screenshot last # Last region (pre-selected)
|
||||||
dms screenshot --reset # Reset last region pre-selection
|
|
||||||
dms screenshot --no-clipboard # Save file only
|
dms screenshot --no-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
dms screenshot --no-confirm # Region capture on mouse release
|
|
||||||
dms screenshot --cursor=on # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
}
|
}
|
||||||
@@ -123,8 +119,6 @@ func init() {
|
|||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
screenshotCmd.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
@@ -148,8 +142,6 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config.Clipboard = !ssNoClipboard
|
config.Clipboard = !ssNoClipboard
|
||||||
config.SaveFile = !ssNoFile
|
config.SaveFile = !ssNoFile
|
||||||
config.Notify = !ssNoNotify
|
config.Notify = !ssNoNotify
|
||||||
config.NoConfirm = ssNoConfirm
|
|
||||||
config.Reset = ssReset
|
|
||||||
config.Stdout = ssStdout
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
if ssOutputDir != "" {
|
if ssOutputDir != "" {
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
PersistentPreRunE: requireMutableSystemCommand,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
|
||||||
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
immutablePolicyOnce sync.Once
|
|
||||||
immutablePolicy immutableCommandPolicy
|
|
||||||
immutablePolicyErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/cli-policy.default.json
|
|
||||||
var defaultCLIPolicyJSON []byte
|
|
||||||
|
|
||||||
type immutableCommandPolicy struct {
|
|
||||||
ImmutableSystem bool
|
|
||||||
ImmutableReason string
|
|
||||||
BlockedCommands []string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliPolicyFile struct {
|
|
||||||
PolicyVersion int `json:"policy_version"`
|
|
||||||
ImmutableSystem *bool `json:"immutable_system"`
|
|
||||||
BlockedCommands *[]string `json:"blocked_commands"`
|
|
||||||
Message *string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCommandSpec(raw string) string {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
|
||||||
normalized = strings.TrimPrefix(normalized, "dms ")
|
|
||||||
return strings.Join(strings.Fields(normalized), " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeBlockedCommands(raw []string) []string {
|
|
||||||
normalized := make([]string, 0, len(raw))
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, cmd := range raw {
|
|
||||||
spec := normalizeCommandSpec(cmd)
|
|
||||||
if spec == "" || seen[spec] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[spec] = true
|
|
||||||
normalized = append(normalized, spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
|
||||||
normalizedPath := normalizeCommandSpec(commandPath)
|
|
||||||
if normalizedPath == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range blocked {
|
|
||||||
spec := normalizeCommandSpec(entry)
|
|
||||||
if spec == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var policy cliPolicyFile
|
|
||||||
if err := json.Unmarshal(data, &policy); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &policy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
|
||||||
policyFile, err := loadPolicyFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if policyFile == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if policyFile.ImmutableSystem != nil {
|
|
||||||
base.ImmutableSystem = *policyFile.ImmutableSystem
|
|
||||||
}
|
|
||||||
if policyFile.BlockedCommands != nil {
|
|
||||||
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
|
||||||
}
|
|
||||||
if policyFile.Message != nil {
|
|
||||||
msg := strings.TrimSpace(*policyFile.Message)
|
|
||||||
if msg != "" {
|
|
||||||
base.Message = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOSReleaseMap(path string) map[string]string {
|
|
||||||
values := make(map[string]string)
|
|
||||||
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
|
||||||
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
|
||||||
values[key] = strings.ToLower(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasAnyToken(text string, tokens ...string) bool {
|
|
||||||
if text == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.Contains(text, token) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectImmutableSystem() (bool, string) {
|
|
||||||
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
|
||||||
return true, "/run/ostree-booted is present"
|
|
||||||
}
|
|
||||||
|
|
||||||
osRelease := readOSReleaseMap("/etc/os-release")
|
|
||||||
if len(osRelease) == 0 {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
id := osRelease["ID"]
|
|
||||||
idLike := osRelease["ID_LIKE"]
|
|
||||||
variantID := osRelease["VARIANT_ID"]
|
|
||||||
name := osRelease["NAME"]
|
|
||||||
prettyName := osRelease["PRETTY_NAME"]
|
|
||||||
|
|
||||||
immutableIDs := map[string]bool{
|
|
||||||
"bluefin": true,
|
|
||||||
"bazzite": true,
|
|
||||||
"silverblue": true,
|
|
||||||
"kinoite": true,
|
|
||||||
"sericea": true,
|
|
||||||
"onyx": true,
|
|
||||||
"aurora": true,
|
|
||||||
"fedora-iot": true,
|
|
||||||
"fedora-coreos": true,
|
|
||||||
}
|
|
||||||
if immutableIDs[id] {
|
|
||||||
return true, "os-release ID=" + id
|
|
||||||
}
|
|
||||||
|
|
||||||
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
|
||||||
if hasAnyToken(variantID, markers...) {
|
|
||||||
return true, "os-release VARIANT_ID=" + variantID
|
|
||||||
}
|
|
||||||
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
|
||||||
return true, "os-release ID_LIKE=" + idLike
|
|
||||||
}
|
|
||||||
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
|
||||||
return true, "os-release identifies an atomic/ostree variant"
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
|
||||||
immutablePolicyOnce.Do(func() {
|
|
||||||
detectedImmutable, reason := detectImmutableSystem()
|
|
||||||
immutablePolicy = immutableCommandPolicy{
|
|
||||||
ImmutableSystem: detectedImmutable,
|
|
||||||
ImmutableReason: reason,
|
|
||||||
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
|
||||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultPolicy cliPolicyFile
|
|
||||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
|
||||||
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if defaultPolicy.BlockedCommands != nil {
|
|
||||||
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
|
||||||
}
|
|
||||||
if defaultPolicy.Message != nil {
|
|
||||||
msg := strings.TrimSpace(*defaultPolicy.Message)
|
|
||||||
if msg != "" {
|
|
||||||
immutablePolicy.Message = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
|
||||||
immutablePolicyErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
|
||||||
immutablePolicyErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if immutablePolicyErr != nil {
|
|
||||||
return nil, immutablePolicyErr
|
|
||||||
}
|
|
||||||
return &immutablePolicy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
|
||||||
policy, err := getImmutablePolicy()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !policy.ImmutableSystem {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
|
||||||
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reason := ""
|
|
||||||
if policy.ImmutableReason != "" {
|
|
||||||
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
|
||||||
}
|
|
||||||
+10
-3
@@ -16,14 +16,21 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
// Add subcommands to greeter
|
||||||
authCmd.AddCommand(authSyncCmd)
|
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(authCmd)
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
|
|||||||
@@ -11,22 +11,29 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// Add flags
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
// Add subcommands to greeter
|
||||||
authCmd.AddCommand(authSyncCmd)
|
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.AddCommand(authCmd)
|
|
||||||
|
|
||||||
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.")
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-57
@@ -192,9 +192,6 @@ func runShellInteractive(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
|
||||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
}
|
}
|
||||||
@@ -435,9 +432,6 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
|
||||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
}
|
}
|
||||||
@@ -622,43 +616,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstDMSPID() (int, bool) {
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if proc.Signal(syscall.Signal(0)) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -670,21 +627,10 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
switch pid, ok := getFirstDMSPID(); {
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
case ok:
|
|
||||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
|
||||||
default:
|
|
||||||
if err := findConfig(nil, nil); err != nil {
|
|
||||||
log.Fatalf("Error finding config: %v", err)
|
|
||||||
}
|
|
||||||
// ! TODO - remove check when QS 0.3 is released
|
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
|
||||||
}
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
cmdArgs = append(cmdArgs, args...)
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func findCommandPath(cmd string) (string, error) {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.0
|
||||||
|
|
||||||
toolchain go1.26.1
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package blur
|
|
||||||
|
|
||||||
import (
|
|
||||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
|
||||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
|
||||||
|
|
||||||
func ProbeSupport() (bool, error) {
|
|
||||||
display, err := client.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer display.Context().Close()
|
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case extBackgroundEffectInterface:
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
@@ -14,37 +12,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
return CopyOpts(data, mimeType, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
total := 0
|
|
||||||
for total < len(data) {
|
|
||||||
n, err := writer.Write(data[total:])
|
|
||||||
total += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if total != len(data) {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
if !foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServeReader(data, mimeType, pasteOnce)
|
return copyServe(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
args = append(args, "--paste-once")
|
args = append(args, "--paste-once")
|
||||||
@@ -52,102 +30,30 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
args = append(args, "--type", mimeType)
|
args = append(args, "--type", mimeType)
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch src := data.(type) {
|
if err := cmd.Start(); err != nil {
|
||||||
case *os.File:
|
return fmt.Errorf("start: %w", err)
|
||||||
cmd.Stdin = src
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(stdin, data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
|
||||||
if err := stdin.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf [1]byte
|
if _, err := stdin.Write(data); err != nil {
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
stdin.Close()
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
}
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os.Stdout.Write([]byte{1})
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
|
||||||
cachedData, err := createClipboardCacheFile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(cachedData.Name())
|
|
||||||
|
|
||||||
if _, err := io.Copy(cachedData, data); err != nil {
|
|
||||||
return fmt.Errorf("cache clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
if err := cachedData.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
cachedFile, err := os.Open(cachedData.Name())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer cachedFile.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
|
||||||
return fmt.Errorf("write clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
|
||||||
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)
|
||||||
@@ -233,18 +139,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
sendErr := make(chan error, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
defer syscall.Close(e.Fd)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := writeTo(file); err != nil {
|
file.Write(data)
|
||||||
select {
|
|
||||||
case sendErr <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -260,14 +160,11 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
signalReady()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
case err := <-sendErr:
|
|
||||||
return err
|
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -252,7 +252,6 @@ window-rule {
|
|||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
match app-id=r#"com.danklinux.dms$"#
|
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
+62
-109
@@ -135,42 +135,6 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
|
||||||
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
|
||||||
data, err := os.ReadFile(srcinfoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
var pkg string
|
|
||||||
var target *[]string
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(line, "makedepends = "):
|
|
||||||
pkg = strings.TrimPrefix(line, "makedepends = ")
|
|
||||||
target = &makedeps
|
|
||||||
case strings.HasPrefix(line, "depends = "):
|
|
||||||
pkg = strings.TrimPrefix(line, "depends = ")
|
|
||||||
target = &deps
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
|
||||||
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
|
||||||
pkg = pkg[:idx]
|
|
||||||
}
|
|
||||||
pkg = strings.TrimSpace(pkg)
|
|
||||||
if pkg != "" {
|
|
||||||
*target = append(*target, pkg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deps, makedeps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
|
||||||
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -476,10 +440,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
|
|||||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
hasNiri := false
|
hasNiri := false
|
||||||
|
hasQuickshell := false
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "niri-git" {
|
if pkg == "niri-git" {
|
||||||
hasNiri = true
|
hasNiri = true
|
||||||
}
|
}
|
||||||
|
if pkg == "quickshell" || pkg == "quickshell-git" {
|
||||||
|
hasQuickshell = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If quickshell is in the list, always reinstall google-breakpad first
|
||||||
|
if hasQuickshell {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.63,
|
||||||
|
Step: "Reinstalling google-breakpad for quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
||||||
|
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
@@ -560,16 +543,6 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
|
||||||
if visited[pkg] {
|
|
||||||
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
visited[pkg] = true
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -643,8 +616,48 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||||
if pkg == "dms-shell-bin" {
|
// since we manually manage those dependencies
|
||||||
|
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||||
|
// Pre-install dependencies from .SRCINFO
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install dependencies and makedepends explicitly
|
||||||
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
|
|
||||||
|
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf(`
|
||||||
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
if [[ "%s" == *"quickshell"* ]]; then
|
||||||
|
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
||||||
|
fi
|
||||||
|
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||||
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||||
|
fi
|
||||||
|
`, srcinfoPath, pkg, sudoPassword))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf(`
|
||||||
|
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
||||||
|
if [ ! -z "$makedeps" ]; then
|
||||||
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
||||||
|
fi
|
||||||
|
`, srcinfoPath, sudoPassword))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
@@ -652,66 +665,6 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Classifying dependencies as system or AUR",
|
|
||||||
}
|
|
||||||
|
|
||||||
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
var systemPkgs []string
|
|
||||||
var aurPkgs []string
|
|
||||||
|
|
||||||
for _, dep := range append(runtimeDeps, makeDeps...) {
|
|
||||||
if seen[dep] || a.packageInstalled(dep) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[dep] = true
|
|
||||||
if a.isInSystemRepo(dep) {
|
|
||||||
systemPkgs = append(systemPkgs, dep)
|
|
||||||
} else {
|
|
||||||
aurPkgs = append(aurPkgs, dep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(systemPkgs) > 0 {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.32*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
|
||||||
}
|
|
||||||
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, aurDep := range aurPkgs {
|
|
||||||
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
|
||||||
}
|
|
||||||
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
|
||||||
startProgress+0.35*(endProgress-startProgress),
|
|
||||||
startProgress+0.39*(endProgress-startProgress),
|
|
||||||
visited,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -724,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
buildCmd.Dir = packageDir
|
buildCmd.Dir = packageDir
|
||||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
||||||
|
|
||||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -91,27 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
return debianPackageInstalledPrecisely(pkg)
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
}
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
func debianPackageInstalledPrecisely(pkg string) bool {
|
|
||||||
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(output)) == "installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
func debianRepoArchitecture(arch string) string {
|
|
||||||
switch arch {
|
|
||||||
case "amd64", "x86_64":
|
|
||||||
return "amd64"
|
|
||||||
case "arm64", "aarch64":
|
|
||||||
return "arm64"
|
|
||||||
default:
|
|
||||||
return arch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -211,12 +194,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: "Installing development dependencies...",
|
Step: "Installing development dependencies...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -396,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-install-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
@@ -461,7 +436,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -507,46 +482,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
totalGroups := len(groups)
|
args = append(args, packages...)
|
||||||
|
|
||||||
groupIndex := 0
|
progressChan <- InstallProgressMsg{
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
Phase: PhaseSystemPackages,
|
||||||
if len(groupPackages) == 0 {
|
Progress: 0.40,
|
||||||
return nil
|
Step: "Installing system packages...",
|
||||||
}
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
groupIndex++
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
startProgress := 0.40
|
|
||||||
endProgress := 0.60
|
|
||||||
if totalGroups > 1 {
|
|
||||||
if groupIndex == 1 {
|
|
||||||
endProgress = 0.50
|
|
||||||
} else {
|
|
||||||
startProgress = 0.50
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := d.aptInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: startProgress,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -484,7 +484,28 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -494,57 +515,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
if minimal {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return append(args, packages...)
|
|
||||||
}
|
args = append(args, packages...)
|
||||||
|
|
||||||
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
progressChan <- InstallProgressMsg{
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
Phase: PhaseAURPackages,
|
||||||
totalGroups := len(groups)
|
Progress: 0.70,
|
||||||
|
Step: "Installing COPR packages...",
|
||||||
groupIndex := 0
|
IsComplete: false,
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
NeedsSudo: true,
|
||||||
if len(groupPackages) == 0 {
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
groupIndex++
|
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := f.dnfInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range groups {
|
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package distros
|
|
||||||
|
|
||||||
type minimalInstallGroup struct {
|
|
||||||
packages []string
|
|
||||||
minimal bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldPreferMinimalInstall(pkg string) bool {
|
|
||||||
switch pkg {
|
|
||||||
case "niri", "niri-git":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if shouldPreferMinimalInstall(pkg) {
|
|
||||||
minimal = append(minimal, pkg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
normal = append(normal, pkg)
|
|
||||||
}
|
|
||||||
return normal, minimal
|
|
||||||
}
|
|
||||||
|
|
||||||
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
|
||||||
normal, minimal := splitMinimalInstallPackages(packages)
|
|
||||||
groups := make([]minimalInstallGroup, 0, 2)
|
|
||||||
if len(minimal) > 0 {
|
|
||||||
groups = append(groups, minimalInstallGroup{
|
|
||||||
packages: minimal,
|
|
||||||
minimal: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(normal) > 0 {
|
|
||||||
groups = append(groups, minimalInstallGroup{
|
|
||||||
packages: normal,
|
|
||||||
minimal: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -30,8 +29,6 @@ type OpenSUSEDistribution struct {
|
|||||||
config DistroConfig
|
config DistroConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
|
||||||
|
|
||||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
base := NewBaseDistribution(logChan)
|
base := NewBaseDistribution(logChan)
|
||||||
return &OpenSUSEDistribution{
|
return &OpenSUSEDistribution{
|
||||||
@@ -202,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
return []string{}
|
return []string{
|
||||||
|
"make",
|
||||||
|
"unzip",
|
||||||
|
"gcc",
|
||||||
|
"gcc-c++",
|
||||||
|
"cmake",
|
||||||
|
"ninja",
|
||||||
|
"pkgconf-pkg-config",
|
||||||
|
"git",
|
||||||
|
"qt6-base-devel",
|
||||||
|
"qt6-declarative-devel",
|
||||||
|
"qt6-declarative-private-devel",
|
||||||
|
"qt6-shadertools",
|
||||||
|
"qt6-shadertools-devel",
|
||||||
|
"qt6-wayland-devel",
|
||||||
|
"qt6-waylandclient-private-devel",
|
||||||
|
"spirv-tools-devel",
|
||||||
|
"cli11-devel",
|
||||||
|
"wayland-protocols-devel",
|
||||||
|
"libgbm-devel",
|
||||||
|
"libdrm-devel",
|
||||||
|
"pipewire-devel",
|
||||||
|
"jemalloc-devel",
|
||||||
|
"wayland-utils",
|
||||||
|
"Mesa-libGLESv3-devel",
|
||||||
|
"pam-devel",
|
||||||
|
"glib2-devel",
|
||||||
|
"polkit-devel",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -272,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting prerequisite check...",
|
LogOutput: "Starting prerequisite check...",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
@@ -306,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
|
||||||
|
|
||||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
|
||||||
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{openSUSENiriWaylandServerPackage}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
|
||||||
for _, pkg := range extraPkgs {
|
|
||||||
if slices.Contains(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 {
|
||||||
@@ -516,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenSUSEInstallMediaURI(uri string) bool {
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
|
||||||
|
|
||||||
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "hd:/") ||
|
|
||||||
strings.HasPrefix(normalizedURI, "iso:/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseZypperInstallMediaAliases(output string) []string {
|
|
||||||
var aliases []string
|
|
||||||
|
|
||||||
for _, line := range strings.Split(output, "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || !strings.Contains(line, "|") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(line, "|")
|
|
||||||
if len(parts) < 7 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range parts {
|
|
||||||
parts[i] = strings.TrimSpace(parts[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
alias := parts[1]
|
|
||||||
enabled := strings.ToLower(parts[3])
|
|
||||||
uri := parts[len(parts)-1]
|
|
||||||
|
|
||||||
if alias == "" || strings.EqualFold(alias, "alias") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if enabled != "" && enabled != "yes" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !isOpenSUSEInstallMediaURI(uri) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases = append(aliases, alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
return aliases
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
|
||||||
output, err := listCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
|
||||||
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
aliases := parseZypperInstallMediaAliases(string(output))
|
|
||||||
if len(aliases) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhasePrerequisites,
|
|
||||||
Progress: 0.055,
|
|
||||||
Step: "Disabling install media repositories...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
|
||||||
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, alias := range aliases {
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
|
||||||
repoOutput, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
|
||||||
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
|
||||||
}
|
|
||||||
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
args := []string{"zypper", "install", "-y"}
|
||||||
totalGroups := len(groups)
|
args = append(args, packages...)
|
||||||
|
|
||||||
groupIndex := 0
|
progressChan <- InstallProgressMsg{
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
Phase: PhaseSystemPackages,
|
||||||
if len(groupPackages) == 0 {
|
Progress: 0.40,
|
||||||
return nil
|
Step: "Installing system packages...",
|
||||||
}
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
groupIndex++
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := o.zypperInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
return debianPackageInstalledPrecisely(pkg)
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -452,7 +454,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -461,59 +477,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
args = append(args, packages...)
|
||||||
if minimal {
|
|
||||||
args = append(args, "--no-install-recommends")
|
|
||||||
}
|
|
||||||
return append(args, packages...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
progressChan <- InstallProgressMsg{
|
||||||
groups := orderedMinimalInstallGroups(packages)
|
Phase: PhaseAURPackages,
|
||||||
totalGroups := len(groups)
|
Progress: 0.70,
|
||||||
|
Step: "Installing PPA packages...",
|
||||||
groupIndex := 0
|
IsComplete: false,
|
||||||
installGroup := func(groupPackages []string, minimal bool) error {
|
NeedsSudo: true,
|
||||||
if len(groupPackages) == 0 {
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
groupIndex++
|
|
||||||
groupStart := startProgress
|
|
||||||
groupEnd := endProgress
|
|
||||||
if totalGroups > 1 {
|
|
||||||
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
|
||||||
if groupIndex == 1 {
|
|
||||||
groupEnd = midpoint
|
|
||||||
} else {
|
|
||||||
groupStart = midpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := u.aptInstallArgs(groupPackages, minimal)
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: phase,
|
|
||||||
Progress: groupStart,
|
|
||||||
Step: step,
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range groups {
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
# AppArmor profile for dms-greeter
|
|
||||||
#
|
|
||||||
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
|
||||||
# Manual edits will be overwritten on next sync.
|
|
||||||
#
|
|
||||||
# Mode: complain (denials are logged, nothing is blocked)
|
|
||||||
# To switch to enforce after validating with `aa-logprof`:
|
|
||||||
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
|
||||||
#
|
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
|
||||||
#include <abstractions/base>
|
|
||||||
#include <abstractions/bash>
|
|
||||||
|
|
||||||
# The launcher script itself
|
|
||||||
/usr/bin/dms-greeter r,
|
|
||||||
|
|
||||||
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
|
||||||
/var/cache/dms-greeter/ rw,
|
|
||||||
/var/cache/dms-greeter/** rwlk,
|
|
||||||
|
|
||||||
# DMS config — packaged path
|
|
||||||
/usr/share/quickshell/dms-greeter/ r,
|
|
||||||
/usr/share/quickshell/dms-greeter/** r,
|
|
||||||
/usr/share/quickshell/ r,
|
|
||||||
/usr/share/quickshell/** r,
|
|
||||||
|
|
||||||
# DMS config — system and user overrides
|
|
||||||
/etc/dms/ r,
|
|
||||||
/etc/dms/** r,
|
|
||||||
/usr/share/dms/ r,
|
|
||||||
/usr/share/dms/** r,
|
|
||||||
/home/*/.config/quickshell/ r,
|
|
||||||
/home/*/.config/quickshell/** r,
|
|
||||||
/root/.config/quickshell/ r,
|
|
||||||
/root/.config/quickshell/** r,
|
|
||||||
|
|
||||||
# greetd / PAM — read-only for session setup
|
|
||||||
/etc/greetd/ r,
|
|
||||||
/etc/greetd/** r,
|
|
||||||
/etc/pam.d/ r,
|
|
||||||
/etc/pam.d/** r,
|
|
||||||
/usr/lib/pam.d/ r,
|
|
||||||
/usr/lib/pam.d/** r,
|
|
||||||
|
|
||||||
# Compositor binaries — run unconfined so each compositor uses its own profile
|
|
||||||
/usr/bin/niri Ux,
|
|
||||||
/usr/bin/hyprland Ux,
|
|
||||||
/usr/bin/Hyprland Ux,
|
|
||||||
/usr/bin/sway Ux,
|
|
||||||
/usr/bin/labwc Ux,
|
|
||||||
/usr/bin/scroll Ux,
|
|
||||||
/usr/bin/miracle-wm Ux,
|
|
||||||
/usr/bin/mango Ux,
|
|
||||||
|
|
||||||
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
|
||||||
/usr/bin/qs Ux,
|
|
||||||
/usr/bin/quickshell Ux,
|
|
||||||
|
|
||||||
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
|
||||||
/run/user/[0-9]*/ rw,
|
|
||||||
/run/user/[0-9]*/** rw,
|
|
||||||
|
|
||||||
# DRM / GPU devices (required for Wayland compositor startup)
|
|
||||||
/dev/dri/ r,
|
|
||||||
/dev/dri/* rw,
|
|
||||||
/dev/udmabuf rw,
|
|
||||||
|
|
||||||
# Input devices
|
|
||||||
/dev/input/ r,
|
|
||||||
/dev/input/* r,
|
|
||||||
|
|
||||||
# Systemd journal / logging
|
|
||||||
/run/systemd/journal/socket rw,
|
|
||||||
/dev/log rw,
|
|
||||||
|
|
||||||
# Shell helper binaries invoked by the launcher script
|
|
||||||
/usr/bin/env ix,
|
|
||||||
/usr/bin/mkdir ix,
|
|
||||||
/usr/bin/cat ix,
|
|
||||||
/usr/bin/grep ix,
|
|
||||||
/usr/bin/dirname ix,
|
|
||||||
/usr/bin/basename ix,
|
|
||||||
/usr/bin/command ix,
|
|
||||||
/bin/env ix,
|
|
||||||
/bin/mkdir ix,
|
|
||||||
|
|
||||||
# Signal management (compositor lifecycle)
|
|
||||||
signal (send, receive) set=("term", "int", "hup", "kill"),
|
|
||||||
}
|
|
||||||
+115
-919
File diff suppressed because it is too large
Load Diff
@@ -1,98 +0,0 @@
|
|||||||
package greeter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeTestFile(t *testing.T, path string, content string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write %s: %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
settingsJSON string
|
|
||||||
sessionJSON string
|
|
||||||
wantSourcePath string
|
|
||||||
wantResolvedWallpaper string
|
|
||||||
wantDynamicOverrideUsed bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "dynamic",
|
|
||||||
"greeterWallpaperPath": "Pictures/blue.jpg",
|
|
||||||
"matugenScheme": "scheme-tonal-spot",
|
|
||||||
"iconTheme": "Papirus"
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":true}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
|
||||||
wantDynamicOverrideUsed: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dynamic theme without override uses desktop colors",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "dynamic",
|
|
||||||
"greeterWallpaperPath": ""
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":false}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: "",
|
|
||||||
wantDynamicOverrideUsed: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
|
||||||
settingsJSON: `{
|
|
||||||
"currentThemeName": "purple",
|
|
||||||
"greeterWallpaperPath": "/tmp/blue.jpg"
|
|
||||||
}`,
|
|
||||||
sessionJSON: `{"isLightMode":false}`,
|
|
||||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
|
||||||
wantResolvedWallpaper: "/tmp/blue.jpg",
|
|
||||||
wantDynamicOverrideUsed: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
homeDir := t.TempDir()
|
|
||||||
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
|
||||||
writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
|
||||||
|
|
||||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
|
||||||
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
|
||||||
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
|
||||||
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
|
||||||
}
|
|
||||||
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
|
||||||
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
|
||||||
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -71,7 +71,6 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
@@ -99,9 +98,7 @@ type Options struct {
|
|||||||
Mode ColorMode
|
Mode ColorMode
|
||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
Contrast float64
|
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
ColorsOnly bool
|
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -229,7 +226,6 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
|
|
||||||
log.Info("Running matugen color hex with stock color overrides")
|
log.Info("Running matugen color hex with stock color overrides")
|
||||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
args = appendContrastArg(args, opts.Contrast)
|
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -266,7 +262,6 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
args = []string{opts.Kind, opts.Value}
|
args = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
args = appendContrastArg(args, opts.Contrast)
|
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -278,10 +273,6 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ColorsOnly {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if isDMSGTKActive(opts.ConfigDir) {
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case ColorModeLight:
|
case ColorModeLight:
|
||||||
@@ -302,13 +293,6 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendContrastArg(args []string, contrast float64) []string {
|
|
||||||
if contrast == 0 {
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||||
|
|
||||||
@@ -346,10 +330,6 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
if opts.ColorsOnly {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -616,10 +596,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
|||||||
matugenVersionOK = true
|
matugenVersionOK = true
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenSupportsCOE {
|
||||||
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||||
}
|
}
|
||||||
if matugenIsV4 {
|
if matugenIsV4 {
|
||||||
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
log.Infof("Matugen %s: using v4 flags", versionStr)
|
||||||
}
|
}
|
||||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
@@ -697,7 +677,6 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
|||||||
baseArgs = []string{opts.Kind, opts.Value}
|
baseArgs = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
|
||||||
if flags.isV4 {
|
if flags.isV4 {
|
||||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -393,51 +392,3 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
|
||||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseConfig := "[config]\ncustom_keywords = []\n"
|
|
||||||
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write base config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp config: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(cfgFile.Name())
|
|
||||||
defer cfgFile.Close()
|
|
||||||
|
|
||||||
opts := &Options{
|
|
||||||
ShellDir: shellDir,
|
|
||||||
ConfigDir: filepath.Join(tempDir, "config"),
|
|
||||||
StateDir: filepath.Join(tempDir, "state"),
|
|
||||||
ColorsOnly: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
|
||||||
t.Fatalf("buildMergedConfig failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfgFile.Close(); err != nil {
|
|
||||||
t.Fatalf("failed to close merged config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := os.ReadFile(cfgFile.Name())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read merged config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
content := string(output)
|
|
||||||
assert.Contains(t, content, "[templates.dank]")
|
|
||||||
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
|
||||||
assert.NotContains(t, content, "[templates.gtk]")
|
|
||||||
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -60,11 +59,7 @@ func Send(n Notification) error {
|
|||||||
|
|
||||||
hints := map[string]dbus.Variant{}
|
hints := map[string]dbus.Variant{}
|
||||||
if n.FilePath != "" {
|
if n.FilePath != "" {
|
||||||
imgPath := n.FilePath
|
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
||||||
if !strings.HasPrefix(imgPath, "file://") {
|
|
||||||
imgPath = "file://" + imgPath
|
|
||||||
}
|
|
||||||
hints["image_path"] = dbus.MakeVariant(imgPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := conn.Object(notifyDest, notifyPath)
|
obj := conn.Object(notifyDest, notifyPath)
|
||||||
|
|||||||
@@ -1,892 +0,0 @@
|
|||||||
package pam
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
|
|
||||||
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
|
|
||||||
|
|
||||||
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
|
|
||||||
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
|
|
||||||
|
|
||||||
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
|
|
||||||
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
|
|
||||||
|
|
||||||
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
|
|
||||||
legacyGreeterPamU2FComment = "# DMS greeter U2F"
|
|
||||||
|
|
||||||
GreetdPamPath = "/etc/pam.d/greetd"
|
|
||||||
DankshellPamPath = "/etc/pam.d/dankshell"
|
|
||||||
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
|
|
||||||
)
|
|
||||||
|
|
||||||
var includedPamAuthFiles = []string{
|
|
||||||
"system-auth",
|
|
||||||
"common-auth",
|
|
||||||
"password-auth",
|
|
||||||
"system-login",
|
|
||||||
"system-local-login",
|
|
||||||
"common-auth-pc",
|
|
||||||
"login",
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthSettings struct {
|
|
||||||
EnableFprint bool `json:"enableFprint"`
|
|
||||||
EnableU2f bool `json:"enableU2f"`
|
|
||||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
|
||||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyncAuthOptions struct {
|
|
||||||
HomeDir string
|
|
||||||
ForceGreeterAuth bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type syncDeps struct {
|
|
||||||
pamDir string
|
|
||||||
greetdPath string
|
|
||||||
dankshellPath string
|
|
||||||
dankshellU2fPath string
|
|
||||||
isNixOS func() bool
|
|
||||||
readFile func(string) ([]byte, error)
|
|
||||||
stat func(string) (os.FileInfo, error)
|
|
||||||
createTemp func(string, string) (*os.File, error)
|
|
||||||
removeFile func(string) error
|
|
||||||
runSudoCmd func(string, string, ...string) error
|
|
||||||
pamModuleExists func(string) bool
|
|
||||||
fingerprintAvailableForCurrentUser func() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type lockscreenPamIncludeDirective struct {
|
|
||||||
target string
|
|
||||||
filterType string
|
|
||||||
}
|
|
||||||
|
|
||||||
type lockscreenPamResolver struct {
|
|
||||||
pamDir string
|
|
||||||
readFile func(string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultSyncDeps() syncDeps {
|
|
||||||
return syncDeps{
|
|
||||||
pamDir: "/etc/pam.d",
|
|
||||||
greetdPath: GreetdPamPath,
|
|
||||||
dankshellPath: DankshellPamPath,
|
|
||||||
dankshellU2fPath: DankshellU2FPamPath,
|
|
||||||
isNixOS: IsNixOS,
|
|
||||||
readFile: os.ReadFile,
|
|
||||||
stat: os.Stat,
|
|
||||||
createTemp: os.CreateTemp,
|
|
||||||
removeFile: os.Remove,
|
|
||||||
runSudoCmd: runSudoCmd,
|
|
||||||
pamModuleExists: pamModuleExists,
|
|
||||||
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsNixOS() bool {
|
|
||||||
_, err := os.Stat("/etc/NIXOS")
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadAuthSettings(homeDir string) (AuthSettings, error) {
|
|
||||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
|
||||||
data, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return AuthSettings{}, nil
|
|
||||||
}
|
|
||||||
return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(string(data)) == "" {
|
|
||||||
return AuthSettings{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings AuthSettings
|
|
||||||
if err := json.Unmarshal(data, &settings); err != nil {
|
|
||||||
return AuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
|
||||||
}
|
|
||||||
return settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
|
||||||
settings, err := ReadAuthSettings(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
|
|
||||||
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
|
|
||||||
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
|
|
||||||
homeDir := strings.TrimSpace(options.HomeDir)
|
|
||||||
if homeDir == "" {
|
|
||||||
var err error
|
|
||||||
homeDir, err = os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := ReadAuthSettings(homeDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := deps.stat(deps.greetdPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
|
||||||
if deps.isNixOS() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := deps.readFile(deps.greetdPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
originalContent := string(data)
|
|
||||||
stripped, removed := stripManagedGreeterPamBlock(originalContent)
|
|
||||||
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
|
||||||
if !removed && !removedLegacy {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
|
||||||
if pamText == "" {
|
|
||||||
return false, false, false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(pamText, "\n")
|
|
||||||
inManaged := false
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
switch trimmed {
|
|
||||||
case GreeterPamManagedBlockStart:
|
|
||||||
managed = true
|
|
||||||
inManaged = true
|
|
||||||
continue
|
|
||||||
case GreeterPamManagedBlockEnd:
|
|
||||||
inManaged = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
|
||||||
legacy = true
|
|
||||||
}
|
|
||||||
if !inManaged {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, "pam_fprintd") {
|
|
||||||
fingerprint = true
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, "pam_u2f") {
|
|
||||||
u2f = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return managed, fingerprint, u2f, legacy
|
|
||||||
}
|
|
||||||
|
|
||||||
func StripManagedGreeterPamContent(pamText string) (string, bool) {
|
|
||||||
stripped, removed := stripManagedGreeterPamBlock(pamText)
|
|
||||||
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
|
||||||
return stripped, removed || removedLegacy
|
|
||||||
}
|
|
||||||
|
|
||||||
func PamTextIncludesFile(pamText, filename string) bool {
|
|
||||||
lines := strings.Split(pamText, "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, filename) &&
|
|
||||||
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func PamFileHasModule(pamFilePath, module string) bool {
|
|
||||||
data, err := os.ReadFile(pamFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return pamContentHasModule(string(data), module)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DetectIncludedPamModule(pamText, module string) string {
|
|
||||||
return detectIncludedPamModule(pamText, module, defaultSyncDeps())
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectIncludedPamModule(pamText, module string, deps syncDeps) string {
|
|
||||||
for _, includedFile := range includedPamAuthFiles {
|
|
||||||
if !PamTextIncludesFile(pamText, includedFile) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := filepath.Join(deps.pamDir, includedFile)
|
|
||||||
data, err := deps.readFile(path)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pamContentHasModule(string(data), module) {
|
|
||||||
return includedFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func pamContentHasModule(content, module string) bool {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(trimmed, module) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasManagedLockscreenPamFile(content string) bool {
|
|
||||||
return strings.Contains(content, LockscreenPamManagedBlockStart) &&
|
|
||||||
strings.Contains(content, LockscreenPamManagedBlockEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasManagedLockscreenU2FPamFile(content string) bool {
|
|
||||||
return strings.Contains(content, LockscreenU2FPamManagedBlockStart) &&
|
|
||||||
strings.Contains(content, LockscreenU2FPamManagedBlockEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pamDirectiveType(line string) string {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
directiveType := strings.TrimPrefix(fields[0], "-")
|
|
||||||
switch directiveType {
|
|
||||||
case "auth", "account", "password", "session":
|
|
||||||
return directiveType
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isExcludedLockscreenPamLine(line string) bool {
|
|
||||||
for _, field := range strings.Fields(line) {
|
|
||||||
if strings.HasPrefix(field, "#") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) {
|
|
||||||
fields := strings.Fields(trimmed)
|
|
||||||
if len(fields) >= 2 && fields[0] == "@include" {
|
|
||||||
return lockscreenPamIncludeDirective{
|
|
||||||
target: fields[1],
|
|
||||||
filterType: inheritedFilter,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") {
|
|
||||||
lineType := pamDirectiveType(trimmed)
|
|
||||||
if lineType == "" {
|
|
||||||
return lockscreenPamIncludeDirective{}, false
|
|
||||||
}
|
|
||||||
return lockscreenPamIncludeDirective{
|
|
||||||
target: fields[2],
|
|
||||||
filterType: lineType,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields) >= 3 && fields[1] == "@include" {
|
|
||||||
lineType := pamDirectiveType(trimmed)
|
|
||||||
if lineType == "" {
|
|
||||||
return lockscreenPamIncludeDirective{}, false
|
|
||||||
}
|
|
||||||
return lockscreenPamIncludeDirective{
|
|
||||||
target: fields[2],
|
|
||||||
filterType: lineType,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return lockscreenPamIncludeDirective{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) {
|
|
||||||
if strings.TrimSpace(target) == "" {
|
|
||||||
return "", fmt.Errorf("empty PAM include target")
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanPamDir := filepath.Clean(pamDir)
|
|
||||||
if filepath.IsAbs(target) {
|
|
||||||
cleanTarget := filepath.Clean(target)
|
|
||||||
if filepath.Dir(cleanTarget) != cleanPamDir {
|
|
||||||
return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target)
|
|
||||||
}
|
|
||||||
return cleanTarget, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanTarget := filepath.Clean(target)
|
|
||||||
if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) {
|
|
||||||
return "", fmt.Errorf("invalid PAM include target: %s", target)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(cleanPamDir, cleanTarget), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) {
|
|
||||||
path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, seen := range stack {
|
|
||||||
if seen == path {
|
|
||||||
chain := append(append([]string{}, stack...), path)
|
|
||||||
display := make([]string, 0, len(chain))
|
|
||||||
for _, item := range chain {
|
|
||||||
display = append(display, filepath.Base(item))
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := r.readFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolved []string
|
|
||||||
for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
|
|
||||||
rawLine = strings.TrimRight(rawLine, "\r")
|
|
||||||
trimmed := strings.TrimSpace(rawLine)
|
|
||||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
|
|
||||||
lineType := pamDirectiveType(trimmed)
|
|
||||||
if filterType != "" && lineType != "" && lineType != filterType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
nested, err := r.resolveService(include.target, include.filterType, append(stack, path))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resolved = append(resolved, nested...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
lineType := pamDirectiveType(trimmed)
|
|
||||||
if lineType == "" {
|
|
||||||
return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed)
|
|
||||||
}
|
|
||||||
if filterType != "" && lineType != filterType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isExcludedLockscreenPamLine(trimmed) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved = append(resolved, rawLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) {
|
|
||||||
resolver := lockscreenPamResolver{
|
|
||||||
pamDir: pamDir,
|
|
||||||
readFile: readFile,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedLines, err := resolver.resolveService("login", "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if len(resolvedLines) == 0 {
|
|
||||||
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAuth := false
|
|
||||||
for _, line := range resolvedLines {
|
|
||||||
if pamDirectiveType(strings.TrimSpace(line)) == "auth" {
|
|
||||||
hasAuth = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasAuth {
|
|
||||||
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("#%PAM-1.0\n")
|
|
||||||
b.WriteString(LockscreenPamManagedBlockStart + "\n")
|
|
||||||
for _, line := range resolvedLines {
|
|
||||||
b.WriteString(line)
|
|
||||||
b.WriteByte('\n')
|
|
||||||
}
|
|
||||||
b.WriteString(LockscreenPamManagedBlockEnd + "\n")
|
|
||||||
return b.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildManagedLockscreenU2FPamContent() string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("#%PAM-1.0\n")
|
|
||||||
b.WriteString(LockscreenU2FPamManagedBlockStart + "\n")
|
|
||||||
b.WriteString("auth required pam_u2f.so cue nouserok timeout=10\n")
|
|
||||||
b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
|
||||||
if deps.isNixOS() {
|
|
||||||
logFunc("ℹ NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
existingData, err := deps.readFile(deps.dankshellPath)
|
|
||||||
if err == nil {
|
|
||||||
if !hasManagedLockscreenPamFile(string(existingData)) {
|
|
||||||
logFunc("ℹ Custom /etc/pam.d/dankshell found (no DMS block). Skipping.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error {
|
|
||||||
if deps.isNixOS() {
|
|
||||||
logFunc("ℹ NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
existingData, err := deps.readFile(deps.dankshellU2fPath)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enabled {
|
|
||||||
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
|
|
||||||
logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err)
|
|
||||||
}
|
|
||||||
logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
|
|
||||||
logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err)
|
|
||||||
}
|
|
||||||
logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripManagedGreeterPamBlock(content string) (string, bool) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
filtered := make([]string, 0, len(lines))
|
|
||||||
inManagedBlock := false
|
|
||||||
removed := false
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed == GreeterPamManagedBlockStart {
|
|
||||||
inManagedBlock = true
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trimmed == GreeterPamManagedBlockEnd {
|
|
||||||
inManagedBlock = false
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inManagedBlock {
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(filtered, "\n"), removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripLegacyGreeterPamLines(content string) (string, bool) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
filtered := make([]string, 0, len(lines))
|
|
||||||
removed := false
|
|
||||||
|
|
||||||
for i := 0; i < len(lines); i++ {
|
|
||||||
trimmed := strings.TrimSpace(lines[i])
|
|
||||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
|
||||||
removed = true
|
|
||||||
if i+1 < len(lines) {
|
|
||||||
nextLine := strings.TrimSpace(lines[i+1])
|
|
||||||
if strings.HasPrefix(nextLine, "auth") &&
|
|
||||||
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, lines[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(filtered, "\n"), removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
|
|
||||||
block := strings.Join(blockLines, "\n")
|
|
||||||
prefix := strings.Join(lines[:i], "\n")
|
|
||||||
suffix := strings.Join(lines[i:], "\n")
|
|
||||||
switch {
|
|
||||||
case prefix == "":
|
|
||||||
return block + "\n" + suffix, nil
|
|
||||||
case suffix == "":
|
|
||||||
return prefix + "\n" + block, nil
|
|
||||||
default:
|
|
||||||
return prefix + "\n" + block + "\n" + suffix, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
|
|
||||||
var wantFprint, wantU2f bool
|
|
||||||
fprintToggleEnabled := forceAuth
|
|
||||||
u2fToggleEnabled := forceAuth
|
|
||||||
if forceAuth {
|
|
||||||
wantFprint = deps.pamModuleExists("pam_fprintd.so")
|
|
||||||
wantU2f = deps.pamModuleExists("pam_u2f.so")
|
|
||||||
} else {
|
|
||||||
fprintToggleEnabled = settings.GreeterEnableFprint
|
|
||||||
u2fToggleEnabled = settings.GreeterEnableU2f
|
|
||||||
fprintModule := deps.pamModuleExists("pam_fprintd.so")
|
|
||||||
u2fModule := deps.pamModuleExists("pam_u2f.so")
|
|
||||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
|
||||||
wantU2f = settings.GreeterEnableU2f && u2fModule
|
|
||||||
if settings.GreeterEnableFprint && !fprintModule {
|
|
||||||
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
|
|
||||||
}
|
|
||||||
if settings.GreeterEnableU2f && !u2fModule {
|
|
||||||
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if deps.isNixOS() {
|
|
||||||
logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
|
|
||||||
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pamData, err := deps.readFile(deps.greetdPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
originalContent := string(pamData)
|
|
||||||
content, _ := stripManagedGreeterPamBlock(originalContent)
|
|
||||||
content, _ = stripLegacyGreeterPamLines(content)
|
|
||||||
|
|
||||||
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
|
|
||||||
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
|
|
||||||
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
|
|
||||||
if wantFprint && includedFprintFile != "" {
|
|
||||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
|
||||||
wantFprint = false
|
|
||||||
}
|
|
||||||
if wantU2f && includedU2fFile != "" {
|
|
||||||
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
|
|
||||||
wantU2f = false
|
|
||||||
}
|
|
||||||
if !wantFprint && includedFprintFile != "" {
|
|
||||||
if fprintToggleEnabled {
|
|
||||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
|
||||||
if fprintAvailableForCurrentUser {
|
|
||||||
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
|
|
||||||
} else {
|
|
||||||
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if fprintAvailableForCurrentUser {
|
|
||||||
logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
|
|
||||||
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
|
|
||||||
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
|
|
||||||
} else {
|
|
||||||
logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
|
|
||||||
logFunc(" Password auth remains the effective login path.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !wantU2f && includedU2fFile != "" {
|
|
||||||
if u2fToggleEnabled {
|
|
||||||
logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
|
|
||||||
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
|
|
||||||
} else {
|
|
||||||
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
|
|
||||||
logFunc(" " + pamManagerHintForCurrentDistro())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wantFprint || wantU2f {
|
|
||||||
blockLines := []string{GreeterPamManagedBlockStart}
|
|
||||||
if wantFprint {
|
|
||||||
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
|
|
||||||
}
|
|
||||||
if wantU2f {
|
|
||||||
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
|
|
||||||
}
|
|
||||||
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
|
|
||||||
|
|
||||||
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if content == originalContent {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
|
|
||||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
|
|
||||||
}
|
|
||||||
if wantFprint || wantU2f {
|
|
||||||
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
|
|
||||||
} else {
|
|
||||||
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
|
|
||||||
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpPath := tmpFile.Name()
|
|
||||||
defer func() {
|
|
||||||
_ = deps.removeFile(tmpPath)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if _, err := tmpFile.WriteString(content); err != nil {
|
|
||||||
tmpFile.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tmpFile.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to set permissions on %s: %w", destPath, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pamManagerHintForCurrentDistro() string {
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
|
||||||
if err != nil {
|
|
||||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
|
||||||
if !exists {
|
|
||||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
|
|
||||||
switch config.Family {
|
|
||||||
case distros.FamilyFedora:
|
|
||||||
return "Disable it in authselect to force password-only greeter login."
|
|
||||||
case distros.FamilyDebian, distros.FamilyUbuntu:
|
|
||||||
return "Disable it in pam-auth-update to force password-only greeter login."
|
|
||||||
default:
|
|
||||||
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pamModuleExists(module string) bool {
|
|
||||||
for _, libDir := range []string{
|
|
||||||
"/usr/lib64/security",
|
|
||||||
"/usr/lib/security",
|
|
||||||
"/lib64/security",
|
|
||||||
"/lib/security",
|
|
||||||
"/lib/x86_64-linux-gnu/security",
|
|
||||||
"/usr/lib/x86_64-linux-gnu/security",
|
|
||||||
"/lib/aarch64-linux-gnu/security",
|
|
||||||
"/usr/lib/aarch64-linux-gnu/security",
|
|
||||||
"/run/current-system/sw/lib64/security",
|
|
||||||
"/run/current-system/sw/lib/security",
|
|
||||||
} {
|
|
||||||
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasEnrolledFingerprintOutput(output string) bool {
|
|
||||||
lower := strings.ToLower(output)
|
|
||||||
if strings.Contains(lower, "no fingers enrolled") ||
|
|
||||||
strings.Contains(lower, "no fingerprints enrolled") ||
|
|
||||||
strings.Contains(lower, "no prints enrolled") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if strings.Contains(lower, "has fingers enrolled") ||
|
|
||||||
strings.Contains(lower, "has fingerprints enrolled") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(lower, "\n") {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(trimmed, "finger:") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func FingerprintAuthAvailableForCurrentUser() bool {
|
|
||||||
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
|
||||||
if username == "" {
|
|
||||||
username = strings.TrimSpace(os.Getenv("USER"))
|
|
||||||
}
|
|
||||||
if username == "" {
|
|
||||||
out, err := exec.Command("id", "-un").Output()
|
|
||||||
if err == nil {
|
|
||||||
username = strings.TrimSpace(string(out))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fingerprintAuthAvailableForUser(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fingerprintAuthAvailableForUser(username string) bool {
|
|
||||||
username = strings.TrimSpace(username)
|
|
||||||
if username == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !pamModuleExists("pam_fprintd.so") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hasEnrolledFingerprintOutput(string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
|
|
||||||
if sudoPassword != "" {
|
|
||||||
fullArgs := append([]string{command}, args...)
|
|
||||||
quotedArgs := make([]string, len(fullArgs))
|
|
||||||
for i, arg := range fullArgs {
|
|
||||||
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
|
||||||
}
|
|
||||||
cmdStr := strings.Join(quotedArgs, " ")
|
|
||||||
|
|
||||||
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
@@ -1,671 +0,0 @@
|
|||||||
package pam
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeTestFile(t *testing.T, path string, content string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatalf("failed to write %s: %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pamTestEnv struct {
|
|
||||||
pamDir string
|
|
||||||
greetdPath string
|
|
||||||
dankshellPath string
|
|
||||||
dankshellU2fPath string
|
|
||||||
tmpDir string
|
|
||||||
homeDir string
|
|
||||||
availableModules map[string]bool
|
|
||||||
fingerprintAvailable bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPamTestEnv(t *testing.T) *pamTestEnv {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
pamDir := filepath.Join(root, "pam.d")
|
|
||||||
tmpDir := filepath.Join(root, "tmp")
|
|
||||||
homeDir := filepath.Join(root, "home")
|
|
||||||
|
|
||||||
for _, dir := range []string{pamDir, tmpDir, homeDir} {
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatalf("failed to create %s: %v", dir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pamTestEnv{
|
|
||||||
pamDir: pamDir,
|
|
||||||
greetdPath: filepath.Join(pamDir, "greetd"),
|
|
||||||
dankshellPath: filepath.Join(pamDir, "dankshell"),
|
|
||||||
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
|
|
||||||
tmpDir: tmpDir,
|
|
||||||
homeDir: homeDir,
|
|
||||||
availableModules: map[string]bool{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
|
|
||||||
t.Helper()
|
|
||||||
writeTestFile(t, filepath.Join(e.pamDir, name), content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
|
|
||||||
t.Helper()
|
|
||||||
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
|
|
||||||
return syncDeps{
|
|
||||||
pamDir: e.pamDir,
|
|
||||||
greetdPath: e.greetdPath,
|
|
||||||
dankshellPath: e.dankshellPath,
|
|
||||||
dankshellU2fPath: e.dankshellU2fPath,
|
|
||||||
isNixOS: func() bool { return isNixOS },
|
|
||||||
readFile: os.ReadFile,
|
|
||||||
stat: os.Stat,
|
|
||||||
createTemp: func(_ string, pattern string) (*os.File, error) {
|
|
||||||
return os.CreateTemp(e.tmpDir, pattern)
|
|
||||||
},
|
|
||||||
removeFile: os.Remove,
|
|
||||||
runSudoCmd: func(_ string, command string, args ...string) error {
|
|
||||||
switch command {
|
|
||||||
case "cp":
|
|
||||||
if len(args) != 2 {
|
|
||||||
return fmt.Errorf("unexpected cp args: %v", args)
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(args[1], data, 0o644)
|
|
||||||
case "chmod":
|
|
||||||
if len(args) != 2 {
|
|
||||||
return fmt.Errorf("unexpected chmod args: %v", args)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "rm":
|
|
||||||
if len(args) != 2 || args[0] != "-f" {
|
|
||||||
return fmt.Errorf("unexpected rm args: %v", args)
|
|
||||||
}
|
|
||||||
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pamModuleExists: func(module string) bool {
|
|
||||||
return e.availableModules[module]
|
|
||||||
},
|
|
||||||
fingerprintAvailableForCurrentUser: func() bool {
|
|
||||||
return e.fingerprintAvailable
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFileString(t *testing.T, path string) string {
|
|
||||||
t.Helper()
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read %s: %v", path, err)
|
|
||||||
}
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHasManagedLockscreenPamFile(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
content string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "both markers present",
|
|
||||||
content: "#%PAM-1.0\n" +
|
|
||||||
LockscreenPamManagedBlockStart + "\n" +
|
|
||||||
"auth sufficient pam_unix.so\n" +
|
|
||||||
LockscreenPamManagedBlockEnd + "\n",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing end marker is not managed",
|
|
||||||
content: "#%PAM-1.0\n" +
|
|
||||||
LockscreenPamManagedBlockStart + "\n" +
|
|
||||||
"auth sufficient pam_unix.so\n",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom file is not managed",
|
|
||||||
content: "#%PAM-1.0\nauth sufficient pam_unix.so\n",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if got := hasManagedLockscreenPamFile(tt.content); got != tt.want {
|
|
||||||
t.Fatalf("hasManagedLockscreenPamFile() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildManagedLockscreenPamContent(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
files map[string]string
|
|
||||||
wantContains []string
|
|
||||||
wantNotContains []string
|
|
||||||
wantCounts map[string]int
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "preserves custom modules and strips direct u2f and fprint directives",
|
|
||||||
files: map[string]string{
|
|
||||||
"login": "#%PAM-1.0\n" +
|
|
||||||
"auth include system-auth\n" +
|
|
||||||
"account include system-auth\n" +
|
|
||||||
"session include system-auth\n",
|
|
||||||
"system-auth": "auth requisite pam_nologin.so\n" +
|
|
||||||
"auth sufficient pam_unix.so try_first_pass nullok\n" +
|
|
||||||
"auth sufficient pam_u2f.so cue\n" +
|
|
||||||
"auth sufficient pam_fprintd.so max-tries=1\n" +
|
|
||||||
"auth required pam_radius_auth.so conf=/etc/raddb/server\n" +
|
|
||||||
"account required pam_access.so\n" +
|
|
||||||
"session optional pam_lastlog.so silent\n",
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
"#%PAM-1.0",
|
|
||||||
LockscreenPamManagedBlockStart,
|
|
||||||
LockscreenPamManagedBlockEnd,
|
|
||||||
"auth requisite pam_nologin.so",
|
|
||||||
"auth sufficient pam_unix.so try_first_pass nullok",
|
|
||||||
"auth required pam_radius_auth.so conf=/etc/raddb/server",
|
|
||||||
"account required pam_access.so",
|
|
||||||
"session optional pam_lastlog.so silent",
|
|
||||||
},
|
|
||||||
wantNotContains: []string{
|
|
||||||
"pam_u2f",
|
|
||||||
"pam_fprintd",
|
|
||||||
},
|
|
||||||
wantCounts: map[string]int{
|
|
||||||
"auth required pam_radius_auth.so conf=/etc/raddb/server": 1,
|
|
||||||
"account required pam_access.so": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "resolves nested include substack and @include transitively",
|
|
||||||
files: map[string]string{
|
|
||||||
"login": "#%PAM-1.0\n" +
|
|
||||||
"auth include system-auth\n" +
|
|
||||||
"account include system-auth\n" +
|
|
||||||
"password include system-auth\n" +
|
|
||||||
"session include system-auth\n",
|
|
||||||
"system-auth": "auth substack custom-auth\n" +
|
|
||||||
"account include custom-auth\n" +
|
|
||||||
"password include custom-auth\n" +
|
|
||||||
"session @include common-session\n",
|
|
||||||
"custom-auth": "auth required pam_custom.so one=two\n" +
|
|
||||||
"account required pam_custom_account.so\n" +
|
|
||||||
"password required pam_custom_password.so\n",
|
|
||||||
"common-session": "session optional pam_fprintd.so max-tries=1\n" +
|
|
||||||
"session optional pam_lastlog.so silent\n",
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
"auth required pam_custom.so one=two",
|
|
||||||
"account required pam_custom_account.so",
|
|
||||||
"password required pam_custom_password.so",
|
|
||||||
"session optional pam_lastlog.so silent",
|
|
||||||
},
|
|
||||||
wantNotContains: []string{
|
|
||||||
"pam_fprintd",
|
|
||||||
},
|
|
||||||
wantCounts: map[string]int{
|
|
||||||
"auth required pam_custom.so one=two": 1,
|
|
||||||
"account required pam_custom_account.so": 1,
|
|
||||||
"password required pam_custom_password.so": 1,
|
|
||||||
"session optional pam_lastlog.so silent": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing include fails",
|
|
||||||
files: map[string]string{
|
|
||||||
"login": "#%PAM-1.0\nauth include missing-auth\n",
|
|
||||||
},
|
|
||||||
wantErr: "failed to read PAM file",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cyclic include fails",
|
|
||||||
files: map[string]string{
|
|
||||||
"login": "#%PAM-1.0\nauth include system-auth\n",
|
|
||||||
"system-auth": "auth include login\n",
|
|
||||||
},
|
|
||||||
wantErr: "cyclic PAM include detected",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no auth directives remain after filtering fails",
|
|
||||||
files: map[string]string{
|
|
||||||
"login": "#%PAM-1.0\nauth include system-auth\n",
|
|
||||||
"system-auth": "auth sufficient pam_u2f.so cue\n",
|
|
||||||
},
|
|
||||||
wantErr: "no auth directives remained after filtering",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
for name, content := range tt.files {
|
|
||||||
env.writePamFile(t, name, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := buildManagedLockscreenPamContent(env.pamDir, os.ReadFile)
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
||||||
t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
if !strings.Contains(content, want) {
|
|
||||||
t.Errorf("missing expected string %q in output:\n%s", want, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, notWant := range tt.wantNotContains {
|
|
||||||
if strings.Contains(content, notWant) {
|
|
||||||
t.Errorf("unexpected string %q found in output:\n%s", notWant, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for want, wantCount := range tt.wantCounts {
|
|
||||||
if gotCount := strings.Count(content, want); gotCount != wantCount {
|
|
||||||
t.Errorf("count for %q = %d, want %d\noutput:\n%s", want, gotCount, wantCount, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncLockscreenPamConfigWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("custom dankshell file is skipped untouched", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
customContent := "#%PAM-1.0\nauth required pam_unix.so\n"
|
|
||||||
env.writePamFile(t, "dankshell", customContent)
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := readFileString(t, env.dankshellPath); got != customContent {
|
|
||||||
t.Fatalf("custom dankshell content changed\ngot:\n%s\nwant:\n%s", got, customContent)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell found") {
|
|
||||||
t.Fatalf("expected custom-file skip log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("managed dankshell file is rewritten from resolved login stack", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\nauth sufficient pam_u2f.so cue\naccount required pam_access.so\n")
|
|
||||||
env.writePamFile(t, "dankshell", "#%PAM-1.0\n"+LockscreenPamManagedBlockStart+"\nauth required pam_env.so\n"+LockscreenPamManagedBlockEnd+"\n")
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output := readFileString(t, env.dankshellPath)
|
|
||||||
for _, want := range []string{
|
|
||||||
LockscreenPamManagedBlockStart,
|
|
||||||
"auth sufficient pam_unix.so try_first_pass nullok",
|
|
||||||
"account required pam_access.so",
|
|
||||||
LockscreenPamManagedBlockEnd,
|
|
||||||
} {
|
|
||||||
if !strings.Contains(output, want) {
|
|
||||||
t.Errorf("missing expected string %q in rewritten dankshell:\n%s", want, output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(output, "pam_u2f") {
|
|
||||||
t.Errorf("rewritten dankshell still contains pam_u2f:\n%s", output)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell") {
|
|
||||||
t.Fatalf("expected success log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("mutable systems fail when login stack cannot be converted safely", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
err := syncLockscreenPamConfigWithDeps(func(string) {}, "", env.deps(false))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error when login PAM file is missing, got nil")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "failed to build") {
|
|
||||||
t.Fatalf("error = %q, want substring %q", err.Error(), "failed to build")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("NixOS remains informational and does not write dankshell", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
var logs []string
|
|
||||||
|
|
||||||
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", env.deps(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncLockscreenPamConfigWithDeps returned error on NixOS path: %v", err)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "/etc/pam.d/login") {
|
|
||||||
t.Fatalf("expected NixOS informational log mentioning /etc/pam.d/login, got %v", logs)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected no dankshell file to be written on NixOS path, stat err = %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("enabled creates managed file", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
var logs []string
|
|
||||||
|
|
||||||
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", true, env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.dankshellU2fPath)
|
|
||||||
if got != buildManagedLockscreenU2FPamContent() {
|
|
||||||
t.Fatalf("unexpected managed dankshell-u2f content:\n%s", got)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell-u2f") {
|
|
||||||
t.Fatalf("expected create log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("enabled rewrites existing managed file", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\n"+LockscreenU2FPamManagedBlockStart+"\nauth required pam_u2f.so old\n"+LockscreenU2FPamManagedBlockEnd+"\n")
|
|
||||||
|
|
||||||
if err := syncLockscreenU2FPamConfigWithDeps(func(string) {}, "", true, env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
|
||||||
t.Fatalf("managed dankshell-u2f was not rewritten:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("disabled removes DMS-managed file", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", false, env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected managed dankshell-u2f to be removed, stat err = %v", err)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Removed DMS-managed /etc/pam.d/dankshell-u2f") {
|
|
||||||
t.Fatalf("expected removal log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("disabled preserves custom file", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
customContent := "#%PAM-1.0\nauth required pam_u2f.so cue\n"
|
|
||||||
env.writePamFile(t, "dankshell-u2f", customContent)
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", false, env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
if got := readFileString(t, env.dankshellU2fPath); got != customContent {
|
|
||||||
t.Fatalf("custom dankshell-u2f content changed\ngot:\n%s\nwant:\n%s", got, customContent)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell-u2f found") {
|
|
||||||
t.Fatalf("expected custom-file log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.availableModules["pam_u2f.so"] = true
|
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
|
||||||
|
|
||||||
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
|
|
||||||
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.greetdPath)
|
|
||||||
for _, want := range []string{
|
|
||||||
GreeterPamManagedBlockStart,
|
|
||||||
"auth sufficient pam_fprintd.so max-tries=1 timeout=5",
|
|
||||||
"auth sufficient pam_u2f.so cue nouserok timeout=10",
|
|
||||||
GreeterPamManagedBlockEnd,
|
|
||||||
} {
|
|
||||||
if !strings.Contains(got, want) {
|
|
||||||
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
|
|
||||||
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.fingerprintAvailable = true
|
|
||||||
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
|
||||||
env.writePamFile(t, "greetd", original)
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
|
|
||||||
|
|
||||||
settings := AuthSettings{GreeterEnableFprint: true}
|
|
||||||
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.greetdPath)
|
|
||||||
if got != original {
|
|
||||||
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
|
|
||||||
}
|
|
||||||
if strings.Contains(got, GreeterPamManagedBlockStart) {
|
|
||||||
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
|
|
||||||
legacyGreeterPamFprintComment+"\n"+
|
|
||||||
"auth sufficient pam_fprintd.so max-tries=1\n"+
|
|
||||||
GreeterPamManagedBlockStart+"\n"+
|
|
||||||
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
|
|
||||||
GreeterPamManagedBlockEnd+"\n"+
|
|
||||||
"auth include system-auth\n")
|
|
||||||
|
|
||||||
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
|
|
||||||
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := readFileString(t, env.greetdPath)
|
|
||||||
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
|
|
||||||
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, "auth include system-auth") {
|
|
||||||
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.writeSettings(t, `{"enableU2f":true}`)
|
|
||||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncAuthConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(env.dankshellPath); err != nil {
|
|
||||||
t.Fatalf("expected dankshell to be created: %v", err)
|
|
||||||
}
|
|
||||||
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
|
||||||
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
|
|
||||||
}
|
|
||||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
|
|
||||||
t.Fatalf("expected greetd skip log, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
|
|
||||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
|
||||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
|
||||||
|
|
||||||
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dankshell := readFileString(t, env.dankshellPath)
|
|
||||||
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
|
|
||||||
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
greetd := readFileString(t, env.greetdPath)
|
|
||||||
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") {
|
|
||||||
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
|
|
||||||
}
|
|
||||||
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
|
|
||||||
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newPamTestEnv(t)
|
|
||||||
env.availableModules["pam_fprintd.so"] = true
|
|
||||||
env.availableModules["pam_u2f.so"] = true
|
|
||||||
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
|
|
||||||
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
|
||||||
env.writePamFile(t, "greetd", originalGreetd)
|
|
||||||
|
|
||||||
var logs []string
|
|
||||||
err := syncAuthConfigWithDeps(func(msg string) {
|
|
||||||
logs = append(logs, msg)
|
|
||||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
|
|
||||||
}
|
|
||||||
if got := readFileString(t, env.greetdPath); got != originalGreetd {
|
|
||||||
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
|
|
||||||
}
|
|
||||||
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
|
|
||||||
t.Fatalf("expected informational NixOS logs, got %v", logs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -444,21 +444,20 @@ func GetFocusedMonitor() string {
|
|||||||
|
|
||||||
type outputInfo struct {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
scale float64
|
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllOutputInfos() map[string]*outputInfo {
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
ctx := display.Context()
|
ctx := display.Context()
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -477,17 +476,16 @@ func getAllOutputInfos() map[string]*outputInfo {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
name string
|
name string
|
||||||
x, y int32
|
x, y int32
|
||||||
scale float64
|
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
@@ -503,9 +501,6 @@ func getAllOutputInfos() map[string]*outputInfo {
|
|||||||
state.x = pe.X
|
state.x = pe.X
|
||||||
state.y = pe.Y
|
state.y = pe.Y
|
||||||
})
|
})
|
||||||
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
|
||||||
state.scale = se.Scale
|
|
||||||
})
|
|
||||||
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
state.transform = te.Transform
|
state.transform = te.Transform
|
||||||
})
|
})
|
||||||
@@ -516,32 +511,21 @@ func getAllOutputInfos() map[string]*outputInfo {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]*outputInfo, len(heads))
|
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == "" {
|
if state.name == outputName {
|
||||||
continue
|
return &outputInfo{
|
||||||
}
|
x: state.x,
|
||||||
result[state.name] = &outputInfo{
|
y: state.y,
|
||||||
x: state.x,
|
transform: state.transform,
|
||||||
y: state.y,
|
}, true
|
||||||
scale: state.scale,
|
|
||||||
transform: state.transform,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
return nil, false
|
||||||
infos := getAllOutputInfos()
|
|
||||||
if infos == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
info, ok := infos[outputName]
|
|
||||||
return info, ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
|||||||
@@ -113,11 +113,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||||
if r.screenshoter != nil && r.screenshoter.config.Reset {
|
r.preSelect = GetLastRegion()
|
||||||
r.preSelect = Region{}
|
|
||||||
} else {
|
|
||||||
r.preSelect = GetLastRegion()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.connect(); err != nil {
|
if err := r.connect(); err != nil {
|
||||||
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
|||||||
@@ -114,9 +114,6 @@ func (r *RegionSelector) setupPointerHandlers() {
|
|||||||
for _, os := range r.surfaces {
|
for _, os := range r.surfaces {
|
||||||
r.redrawSurface(os)
|
r.redrawSurface(os)
|
||||||
}
|
}
|
||||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
|
|
||||||
r.finishSelection()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
r.cancelled = true
|
r.cancelled = true
|
||||||
|
|||||||
@@ -138,13 +138,9 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
|
|||||||
if !r.showCapturedCursor {
|
if !r.showCapturedCursor {
|
||||||
cursorLabel = "show"
|
cursorLabel = "show"
|
||||||
}
|
}
|
||||||
captureKey := "Space/Enter"
|
|
||||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
|
|
||||||
captureKey = "Drag+Release"
|
|
||||||
}
|
|
||||||
|
|
||||||
items := []struct{ key, desc string }{
|
items := []struct{ key, desc string }{
|
||||||
{captureKey, "capture"},
|
{"Space/Enter", "capture"},
|
||||||
{"P", cursorLabel + " cursor"},
|
{"P", cursorLabel + " cursor"},
|
||||||
{"Esc", "cancel"},
|
{"Esc", "cancel"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -107,12 +106,6 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||||
if s.config.Reset {
|
|
||||||
if err := SaveLastRegion(Region{}); err != nil {
|
|
||||||
log.Debug("failed to reset last region", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selector := NewRegionSelector(s)
|
selector := NewRegionSelector(s)
|
||||||
result, cancelled, err := selector.Run()
|
result, cancelled, err := selector.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -305,20 +298,22 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
return nil, fmt.Errorf("no outputs available")
|
return nil, fmt.Errorf("no outputs available")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(outputs) == 1 {
|
if len(outputs) == 1 {
|
||||||
return s.captureWholeOutput(outputs[0])
|
return s.captureWholeOutput(outputs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
wlrInfos := getAllOutputInfos()
|
// Capture all outputs first to get actual buffer sizes
|
||||||
|
type capturedOutput struct {
|
||||||
type pendingOutput struct {
|
output *WaylandOutput
|
||||||
result *CaptureResult
|
result *CaptureResult
|
||||||
logX float64
|
physX int
|
||||||
logY float64
|
physY int
|
||||||
scale float64
|
|
||||||
}
|
}
|
||||||
var pending []pendingOutput
|
captured := make([]capturedOutput, 0, len(outputs))
|
||||||
maxScale := 1.0
|
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
first := true
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -327,74 +322,50 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logX, logY := float64(output.x), float64(output.y)
|
outX, outY := output.x, output.y
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
|
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
logX, logY = float64(hx), float64(hy)
|
outX, outY = hx, hy
|
||||||
}
|
}
|
||||||
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||||
scale = hs
|
scale = s
|
||||||
}
|
}
|
||||||
default:
|
case CompositorDWL:
|
||||||
if wlrInfos != nil {
|
if info, ok := getOutputInfo(output.name); ok {
|
||||||
if info, ok := wlrInfos[output.name]; ok {
|
outX, outY = info.x, info.y
|
||||||
logX, logY = float64(info.x), float64(info.y)
|
|
||||||
if info.scale > 0 {
|
|
||||||
scale = info.scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if scale <= 0 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
physX := int(float64(outX) * scale)
|
||||||
if scale > maxScale {
|
physY := int(float64(outY) * scale)
|
||||||
maxScale = scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pending) == 0 {
|
captured = append(captured, capturedOutput{
|
||||||
return nil, fmt.Errorf("failed to capture any outputs")
|
output: output,
|
||||||
}
|
result: result,
|
||||||
if len(pending) == 1 {
|
physX: physX,
|
||||||
return pending[0].result, nil
|
physY: physY,
|
||||||
}
|
})
|
||||||
|
|
||||||
type layoutEntry struct {
|
right := physX + result.Buffer.Width
|
||||||
result *CaptureResult
|
bottom := physY + result.Buffer.Height
|
||||||
canvasX int
|
|
||||||
canvasY int
|
|
||||||
canvasW int
|
|
||||||
canvasH int
|
|
||||||
}
|
|
||||||
entries := make([]layoutEntry, len(pending))
|
|
||||||
var minX, minY, maxX, maxY int
|
|
||||||
|
|
||||||
for i, p := range pending {
|
if first {
|
||||||
cx := int(math.Round(p.logX * maxScale))
|
minX, minY = physX, physY
|
||||||
cy := int(math.Round(p.logY * maxScale))
|
maxX, maxY = right, bottom
|
||||||
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
first = false
|
||||||
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
|
||||||
|
|
||||||
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
|
||||||
|
|
||||||
right := cx + cw
|
|
||||||
bottom := cy + ch
|
|
||||||
if i == 0 {
|
|
||||||
minX, minY, maxX, maxY = cx, cy, right, bottom
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cx < minX {
|
|
||||||
minX = cx
|
if physX < minX {
|
||||||
|
minX = physX
|
||||||
}
|
}
|
||||||
if cy < minY {
|
if physY < minY {
|
||||||
minY = cy
|
minY = physY
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
maxX = right
|
||||||
@@ -404,26 +375,35 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(captured) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(captured) == 1 {
|
||||||
|
return captured[0].result, nil
|
||||||
|
}
|
||||||
|
|
||||||
totalW := maxX - minX
|
totalW := maxX - minX
|
||||||
totalH := maxY - minY
|
totalH := maxY - minY
|
||||||
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
|
||||||
|
compositeStride := totalW * 4
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, e := range entries {
|
for _, c := range captured {
|
||||||
e.result.Buffer.Close()
|
c.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composite.Clear()
|
composite.Clear()
|
||||||
|
|
||||||
var format uint32
|
var format uint32
|
||||||
for _, e := range entries {
|
for _, c := range captured {
|
||||||
if format == 0 {
|
if format == 0 {
|
||||||
format = e.result.Format
|
format = c.result.Format
|
||||||
}
|
}
|
||||||
s.blitBufferScaled(composite, e.result.Buffer,
|
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
||||||
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
c.result.Buffer.Close()
|
||||||
e.result.YInverted)
|
|
||||||
e.result.Buffer.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -433,44 +413,32 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||||
if dstW <= 0 || dstH <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
srcData := src.Data()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for dy := 0; dy < dstH; dy++ {
|
for srcY := 0; srcY < src.Height; srcY++ {
|
||||||
canvasY := dstY + dy
|
actualSrcY := srcY
|
||||||
if canvasY < 0 || canvasY >= dst.Height {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
srcY := dy * src.Height / dstH
|
|
||||||
if yInverted {
|
if yInverted {
|
||||||
srcY = src.Height - 1 - srcY
|
actualSrcY = src.Height - 1 - srcY
|
||||||
}
|
}
|
||||||
if srcY < 0 || srcY >= src.Height {
|
|
||||||
|
dy := dstY + srcY
|
||||||
|
if dy < 0 || dy >= dst.Height {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := srcY * src.Stride
|
srcRowOff := actualSrcY * src.Stride
|
||||||
dstRowOff := canvasY * dst.Stride
|
dstRowOff := dy * dst.Stride
|
||||||
|
|
||||||
for dx := 0; dx < dstW; dx++ {
|
for srcX := 0; srcX < src.Width; srcX++ {
|
||||||
canvasX := dstX + dx
|
dx := dstX + srcX
|
||||||
if canvasX < 0 || canvasX >= dst.Width {
|
if dx < 0 || dx >= dst.Width {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
srcX := dx * src.Width / dstW
|
|
||||||
if srcX >= src.Width {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + canvasX*4
|
di := dstRowOff + dx*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ type Config struct {
|
|||||||
Mode Mode
|
Mode Mode
|
||||||
OutputName string
|
OutputName string
|
||||||
Cursor CursorMode
|
Cursor CursorMode
|
||||||
NoConfirm bool
|
|
||||||
Reset bool
|
|
||||||
Format Format
|
Format Format
|
||||||
Quality int
|
Quality int
|
||||||
OutputDir string
|
OutputDir string
|
||||||
@@ -68,8 +66,6 @@ func DefaultConfig() Config {
|
|||||||
return Config{
|
return Config{
|
||||||
Mode: ModeRegion,
|
Mode: ModeRegion,
|
||||||
Cursor: CursorOff,
|
Cursor: CursorOff,
|
||||||
NoConfirm: false,
|
|
||||||
Reset: false,
|
|
||||||
Format: FormatPNG,
|
Format: FormatPNG,
|
||||||
Quality: 90,
|
Quality: 90,
|
||||||
OutputDir: "",
|
OutputDir: "",
|
||||||
|
|||||||
@@ -6,20 +6,12 @@ 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
|
||||||
@@ -37,6 +29,13 @@ 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"}},
|
||||||
@@ -49,46 +48,6 @@ 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)
|
||||||
@@ -98,9 +57,10 @@ func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinit
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-m.stop:
|
case <-m.stop:
|
||||||
return nil
|
return
|
||||||
case err := <-errs:
|
case err := <-errs:
|
||||||
return err
|
log.Errorf("Udev monitor error: %v", err)
|
||||||
|
return
|
||||||
case event := <-events:
|
case event := <-events:
|
||||||
m.handleEvent(manager, event)
|
m.handleEvent(manager, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
|
|||||||
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||||
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||||
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||||
Contrast: models.GetOr(req, "contrast", 0.0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wait := models.GetOr(req, "wait", true)
|
wait := models.GetOr(req, "wait", true)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
|
||||||
t.Skipf("system D-Bus unavailable: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.NotNil(t, result) {
|
assert.NotNil(t, result)
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ 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"
|
||||||
|
|
||||||
@@ -192,7 +191,7 @@ func InitializeFreedeskManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeWaylandManager() error {
|
func InitializeWaylandManager(geoClient geolocation.Client) error {
|
||||||
log.Info("Attempting to initialize Wayland gamma control...")
|
log.Info("Attempting to initialize Wayland gamma control...")
|
||||||
|
|
||||||
if wlContext == nil {
|
if wlContext == nil {
|
||||||
@@ -205,7 +204,7 @@ func InitializeWaylandManager() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config := wayland.DefaultConfig()
|
config := wayland.DefaultConfig()
|
||||||
manager, err := wayland.NewManager(wlContext.Display(), config)
|
manager, err := wayland.NewManager(wlContext.Display(), geoClient, 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
|
||||||
@@ -386,8 +385,8 @@ func InitializeDbusManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeThemeModeManager() error {
|
func InitializeThemeModeManager(geoClient geolocation.Client) error {
|
||||||
manager := thememode.NewManager()
|
manager := thememode.NewManager(geoClient)
|
||||||
themeModeManager = manager
|
themeModeManager = manager
|
||||||
|
|
||||||
log.Info("Theme mode automation manager initialized")
|
log.Info("Theme mode automation manager initialized")
|
||||||
@@ -1331,9 +1330,6 @@ 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 {
|
||||||
@@ -1549,6 +1545,9 @@ 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 {
|
||||||
@@ -1593,41 +1592,10 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := InitializeWaylandManager(); err != nil {
|
if err := InitializeWaylandManager(geoClient); 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)
|
||||||
@@ -1656,6 +1624,25 @@ 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() *Manager {
|
func NewManager(geoClient geolocation.Client) *Manager {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
config: Config{
|
config: Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -54,6 +54,7 @@ func NewManager() *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())
|
||||||
@@ -314,10 +315,6 @@ 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
|
||||||
@@ -325,9 +322,6 @@ 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, config Config) (*Manager, error) {
|
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
|||||||
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 {
|
||||||
@@ -421,10 +422,6 @@ 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
|
||||||
@@ -433,31 +430,27 @@ 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
|
m.locationMutex.RLock()
|
||||||
}
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
if m.geoClient == nil {
|
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||||
return nil, nil
|
m.locationMutex.RUnlock()
|
||||||
}
|
return lat, lon
|
||||||
|
}
|
||||||
m.locationMutex.RLock()
|
|
||||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
|
||||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
|
||||||
m.locationMutex.RUnlock()
|
m.locationMutex.RUnlock()
|
||||||
return lat, lon
|
|
||||||
}
|
|
||||||
m.locationMutex.RUnlock()
|
|
||||||
|
|
||||||
location, err := m.geoClient.GetLocation()
|
location, err := m.geoClient.GetLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.locationMutex.Lock()
|
m.locationMutex.Lock()
|
||||||
m.cachedIPLat = &location.Latitude
|
m.cachedIPLat = &location.Latitude
|
||||||
m.cachedIPLon = &location.Longitude
|
m.cachedIPLon = &location.Longitude
|
||||||
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 {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,18 +391,20 @@ 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, config)
|
_, err := NewManager(mockDisplay, mockGeoclient, 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,
|
||||||
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
|||||||
Gamma: 1.0,
|
Gamma: 1.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay, config)
|
_, err := NewManager(mockDisplay, mockGeoclient, 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,9 +123,6 @@ 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()
|
||||||
|
|
||||||
@@ -156,19 +153,9 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
||||||
consecutiveErrors++
|
log.Errorf("Wayland connection error: %v", err)
|
||||||
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
|
return
|
||||||
|
|
||||||
if consecutiveErrors >= maxConsecutiveErrors {
|
|
||||||
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
consecutiveErrors = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
|||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
cmdq: make(chan cmd, 512),
|
cmdq: make(chan cmd, 128),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
fatalError: make(chan error, 1),
|
fatalError: make(chan error, 1),
|
||||||
|
|||||||
@@ -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...")
|
||||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
b.WriteString(fmt.Sprintf("%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...")
|
||||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
b.WriteString(fmt.Sprintf("%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)
|
||||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
b.WriteString(fmt.Sprintf("%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] != "" {
|
||||||
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
|
b.WriteString(fmt.Sprintf("%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...")
|
||||||
fmt.Fprintf(&b, "%s %s", spinner, status)
|
b.WriteString(fmt.Sprintf("%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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&b, " %s %s\n",
|
b.WriteString(fmt.Sprintf(" %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...")
|
||||||
fmt.Fprintf(&b, "%s %s\n\n", spinner, loading)
|
b.WriteString(fmt.Sprintf("%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
|
||||||
Suggests: niri | hyprland | sway
|
Recommends: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
|||||||
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
|
||||||
Package: dms
|
Package: dms
|
||||||
Architecture: amd64 arm64
|
Architecture: amd64
|
||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
dms-distropkg-amd64.gz
|
dms-distropkg-amd64.gz
|
||||||
dms-distropkg-arm64.gz
|
|
||||||
dms-source.tar.gz
|
dms-source.tar.gz
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# Include files that are normally excluded by .gitignore
|
# Include files that are normally excluded by .gitignore
|
||||||
# These are needed for the build process on Launchpad
|
# These are needed for the build process on Launchpad
|
||||||
tar-ignore = !dms-distropkg-amd64.gz
|
tar-ignore = !dms-distropkg-amd64.gz
|
||||||
tar-ignore = !dms-distropkg-arm64.gz
|
|
||||||
tar-ignore = !dms-source.tar.gz
|
tar-ignore = !dms-source.tar.gz
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
%global version {{{ git_repo_version }}}
|
%global version {{{ git_repo_version }}}
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
%global go_toolchain_version 1.26.1
|
%global go_toolchain_version 1.25.7
|
||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Epoch: 2
|
Epoch: 2
|
||||||
|
|||||||
+1
-11
@@ -24,7 +24,6 @@ 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
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -139,13 +138,6 @@ in
|
|||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
|
|
||||||
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
|
|
||||||
# U2F and fingerprint are handled separately by DMS — do not add pam_u2f or pam_fprintd here.
|
|
||||||
# security.pam.services.dankshell = {
|
|
||||||
# # Example: add faillock
|
|
||||||
# faillock.enable = true;
|
|
||||||
# };
|
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = lib.mkDefault true;
|
enable = lib.mkDefault true;
|
||||||
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
||||||
@@ -203,9 +195,7 @@ in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
if [ -f settings.json ]; then
|
||||||
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
|
||||||
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
|
||||||
|
|||||||
+26
-23
@@ -2,9 +2,11 @@
|
|||||||
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
|
||||||
];
|
];
|
||||||
@@ -14,11 +16,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 =
|
enable = (lib.mkEnableOption "includes for niri-flake") // {
|
||||||
(lib.mkEnableOption "includes for niri-flake")
|
default = true;
|
||||||
// {
|
};
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
override = lib.mkOption {
|
override = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = ''
|
description = ''
|
||||||
@@ -44,10 +44,8 @@ in {
|
|||||||
"alttab"
|
"alttab"
|
||||||
"binds"
|
"binds"
|
||||||
"colors"
|
"colors"
|
||||||
"cursor"
|
|
||||||
"layout"
|
"layout"
|
||||||
"outputs"
|
"outputs"
|
||||||
"windowrules"
|
|
||||||
"wpblur"
|
"wpblur"
|
||||||
];
|
];
|
||||||
example = [
|
example = [
|
||||||
@@ -72,21 +70,24 @@ in {
|
|||||||
let
|
let
|
||||||
cfg' = cfg.niri.includes;
|
cfg' = cfg.niri.includes;
|
||||||
|
|
||||||
withOriginalConfig = dmsFiles:
|
withOriginalConfig =
|
||||||
if cfg'.override
|
dmsFiles:
|
||||||
then [cfg'.originalFileName] ++ dmsFiles
|
if cfg'.override then
|
||||||
else dmsFiles ++ [cfg'.originalFileName];
|
[ cfg'.originalFileName ] ++ dmsFiles
|
||||||
|
else
|
||||||
|
dmsFiles ++ [ cfg'.originalFileName ];
|
||||||
|
|
||||||
fixes = map (fix: "\n${fix}") (
|
fixes = map (fix: "\n${fix}") (
|
||||||
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
||||||
# kdl
|
# kdl
|
||||||
''
|
''
|
||||||
// Border fix
|
// Border fix
|
||||||
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
||||||
layout { border { on; }; }
|
layout { border { on; }; }
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
||||||
niri-config-dms = {
|
niri-config-dms = {
|
||||||
target = "niri/config.kdl";
|
target = "niri/config.kdl";
|
||||||
@@ -103,9 +104,11 @@ in {
|
|||||||
|
|
||||||
programs.niri.settings = lib.mkMerge [
|
programs.niri.settings = lib.mkMerge [
|
||||||
(lib.mkIf cfg.niri.enableKeybinds {
|
(lib.mkIf cfg.niri.enableKeybinds {
|
||||||
binds = with config.lib.niri.actions; let
|
binds =
|
||||||
dms-ipc = spawn "dms" "ipc";
|
with config.lib.niri.actions;
|
||||||
in
|
let
|
||||||
|
dms-ipc = spawn "dms" "ipc";
|
||||||
|
in
|
||||||
{
|
{
|
||||||
"Mod+Space" = {
|
"Mod+Space" = {
|
||||||
action = dms-ipc "spotlight" "toggle";
|
action = dms-ipc "spotlight" "toggle";
|
||||||
|
|||||||
@@ -102,19 +102,6 @@ if [[ ! -d "distro/debian" ]]; then
|
|||||||
echo "Error: Run this script from the repository root"
|
echo "Error: Run this script from the repository root"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
|
|
||||||
osc_retry() {
|
|
||||||
local max=3 attempt=1
|
|
||||||
while true; do
|
|
||||||
if osc "$@"; then return 0; fi
|
|
||||||
((attempt >= max)) && return 1
|
|
||||||
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
|
|
||||||
sleep $((5*attempt))
|
|
||||||
((attempt++))
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# $1 = PROJECT
|
# $1 = PROJECT
|
||||||
# $2 = PACKAGE
|
# $2 = PACKAGE
|
||||||
@@ -322,23 +309,8 @@ mkdir -p "$OBS_BASE"
|
|||||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||||
cd "$OBS_BASE"
|
cd "$OBS_BASE"
|
||||||
CHECKOUT_OK=false
|
osc co "$OBS_PROJECT/$PACKAGE"
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if osc co "$OBS_PROJECT/$PACKAGE"; then
|
|
||||||
CHECKOUT_OK=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ $attempt -lt 3 ]]; then
|
|
||||||
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
|
|
||||||
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
|
|
||||||
sleep $((5*attempt))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
|
||||||
echo "Error: Checkout failed after 3 attempts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
||||||
@@ -1092,7 +1064,7 @@ fi
|
|||||||
|
|
||||||
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
||||||
echo "==> Updating working copy"
|
echo "==> Updating working copy"
|
||||||
if ! osc_retry up 2>/dev/null; then
|
if ! osc up 2>/dev/null; then
|
||||||
echo "Error: Failed to update working copy"
|
echo "Error: Failed to update working copy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1173,7 +1145,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
|||||||
else
|
else
|
||||||
echo "==> Committing to OBS"
|
echo "==> Committing to OBS"
|
||||||
set +e
|
set +e
|
||||||
osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
||||||
COMMIT_EXIT=${PIPESTATUS[0]}
|
COMMIT_EXIT=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
# Usage: ./create-source.sh <package-dir> [ubuntu-series]
|
# Usage: ./create-source.sh <package-dir> [ubuntu-series]
|
||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
# ./create-source.sh ../dms questing # Ubuntu 25.10 (default series in ppa-upload)
|
# ./create-source.sh ../dms questing
|
||||||
# ./create-source.sh ../dms resolute # Ubuntu 26.04 LTS
|
|
||||||
# ./create-source.sh ../dms-git questing
|
# ./create-source.sh ../dms-git questing
|
||||||
# ./create-source.sh ../dms-git resolute
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -27,13 +25,11 @@ if [ $# -lt 1 ]; then
|
|||||||
echo "Arguments:"
|
echo "Arguments:"
|
||||||
echo " package-dir : Path to package directory (e.g., ../dms)"
|
echo " package-dir : Path to package directory (e.g., ../dms)"
|
||||||
echo " ubuntu-series : Ubuntu series (optional, default: noble)"
|
echo " ubuntu-series : Ubuntu series (optional, default: noble)"
|
||||||
echo " Options: noble, jammy, oracular, mantic, questing, resolute"
|
echo " Options: noble, jammy, oracular, mantic"
|
||||||
echo
|
echo
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 ../dms questing"
|
echo " $0 ../dms questing"
|
||||||
echo " $0 ../dms resolute"
|
|
||||||
echo " $0 ../dms-git questing"
|
echo " $0 ../dms-git questing"
|
||||||
echo " $0 ../dms-git resolute"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -133,14 +129,10 @@ check_ppa_version_exists() {
|
|||||||
local SOURCE_NAME="$2"
|
local SOURCE_NAME="$2"
|
||||||
local VERSION="$3"
|
local VERSION="$3"
|
||||||
local CHECK_MODE="${4:-commit}"
|
local CHECK_MODE="${4:-commit}"
|
||||||
local DISTRO_SERIES="${5:-}"
|
|
||||||
|
|
||||||
# Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to questing and resolute)
|
# Query Launchpad API
|
||||||
local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
|
PPA_VERSION=$(curl -s \
|
||||||
if [[ -n "$DISTRO_SERIES" ]]; then
|
"https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published" \
|
||||||
API_URL+="&distro_series=https://api.launchpad.net/1.0/ubuntu/${DISTRO_SERIES}"
|
|
||||||
fi
|
|
||||||
PPA_VERSION=$(curl -s "$API_URL" \
|
|
||||||
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
|
|
||||||
if [[ -n "$PPA_VERSION" ]]; then
|
if [[ -n "$PPA_VERSION" ]]; then
|
||||||
@@ -267,14 +259,14 @@ if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
|
|||||||
if [[ -n "$PPA_NAME" ]]; then
|
if [[ -n "$PPA_NAME" ]]; then
|
||||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact" "$UBUNTU_SERIES"; then
|
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact"; then
|
||||||
error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
|
error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
|
||||||
error " To rebuild with a different release number, use:"
|
error " To rebuild with a different release number, use:"
|
||||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact" "$UBUNTU_SERIES"; then
|
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
|
||||||
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
|
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
|
||||||
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
||||||
error " To rebuild with a different release number, use:"
|
error " To rebuild with a different release number, use:"
|
||||||
@@ -418,7 +410,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
|||||||
if [[ -n "$PPA_NAME" ]]; then
|
if [[ -n "$PPA_NAME" ]]; then
|
||||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||||
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
|
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
|
||||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit" "$UBUNTU_SERIES"; then
|
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit"; then
|
||||||
error "==> Error: This commit is already uploaded to PPA"
|
error "==> Error: This commit is already uploaded to PPA"
|
||||||
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
|
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
|
||||||
error " To rebuild the same commit, specify a rebuild number:"
|
error " To rebuild the same commit, specify a rebuild number:"
|
||||||
@@ -437,7 +429,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
|||||||
PPA_NUM=$REBUILD_RELEASE
|
PPA_NUM=$REBUILD_RELEASE
|
||||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact" "$UBUNTU_SERIES"; then
|
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
|
||||||
error "==> Error: Version $NEW_VERSION already exists in PPA"
|
error "==> Error: Version $NEW_VERSION already exists in PPA"
|
||||||
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
|
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
|
||||||
NEXT_NUM=$((PPA_NUM + 1))
|
NEXT_NUM=$((PPA_NUM + 1))
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
|
|
||||||
PPA_OWNER="avengemedia"
|
PPA_OWNER="avengemedia"
|
||||||
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
||||||
# Supported Ubuntu series for PPA builds (25.10 questing + 26.04 LTS resolute)
|
DISTRO_SERIES="questing"
|
||||||
DISTRO_SERIES_LIST=(questing resolute)
|
|
||||||
|
|
||||||
# Define packages (sync with ppa-upload.sh)
|
# Define packages (sync with ppa-upload.sh)
|
||||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
ALL_PACKAGES=(dms dms-git dms-greeter)
|
||||||
@@ -107,10 +106,10 @@ get_status_display() {
|
|||||||
for PPA_NAME in "${PPAS[@]}"; do
|
for PPA_NAME in "${PPAS[@]}"; do
|
||||||
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||||
|
|
||||||
for DISTRO_SERIES in "${DISTRO_SERIES_LIST[@]}"; do
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} (Ubuntu ${DISTRO_SERIES}) ==="
|
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} ==="
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
echo "Distribution: Ubuntu $DISTRO_SERIES"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
for pkg in "${PACKAGES[@]}"; do
|
for pkg in "${PACKAGES[@]}"; do
|
||||||
@@ -211,7 +210,6 @@ for PPA_NAME in "${PPAS[@]}"; do
|
|||||||
|
|
||||||
echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||||
echo ""
|
echo ""
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|||||||
@@ -3,15 +3,13 @@
|
|||||||
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
|
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# ./ppa-upload.sh dms # Upload to questing + resolute (default)
|
# ./ppa-upload.sh dms # Single package (auto-detects PPA)
|
||||||
# ./ppa-upload.sh dms 2 # Native: questing ppa2, resolute ppa3 (auto +1 on second series)
|
# ./ppa-upload.sh dms 2 # Rebuild with ppa2 (simple syntax)
|
||||||
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
|
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
|
||||||
# ./ppa-upload.sh dms-git # Single package (both series)
|
# ./ppa-upload.sh dms-git # Single package
|
||||||
# ./ppa-upload.sh all # All packages (each to both series)
|
# ./ppa-upload.sh all # All packages
|
||||||
# ./ppa-upload.sh dms resolute # 26.04 LTS only (same as "dms dms resolute")
|
# ./ppa-upload.sh dms dms questing # Explicit PPA and series
|
||||||
# ./ppa-upload.sh dms questing # 25.10 only
|
# ./ppa-upload.sh dms dms questing 2 # Explicit PPA, series, and rebuild number
|
||||||
# ./ppa-upload.sh dms dms resolute # Explicit PPA name + one series (optional form)
|
|
||||||
# ./ppa-upload.sh dms dms resolute 2 # One series + rebuild number
|
|
||||||
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
|
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -54,7 +52,7 @@ done
|
|||||||
|
|
||||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||||
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
|
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
||||||
|
|
||||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||||
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
||||||
@@ -66,27 +64,10 @@ if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
|||||||
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
||||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||||
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
|
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Shorthand: "dms resolute" / "dms questing" (package + series; PPA inferred — no need for "dms dms resolute")
|
|
||||||
if [[ ${#POSITIONAL_ARGS[@]} -eq 2 ]] && [[ "${POSITIONAL_ARGS[1]}" == "questing" || "${POSITIONAL_ARGS[1]}" == "resolute" ]]; then
|
|
||||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]}"
|
|
||||||
PPA_NAME_INPUT=""
|
|
||||||
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[1]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERIES_LIST=()
|
|
||||||
if [[ -z "$UBUNTU_SERIES_RAW" ]]; then
|
|
||||||
SERIES_LIST=(questing resolute)
|
|
||||||
elif [[ "$UBUNTU_SERIES_RAW" == "questing" || "$UBUNTU_SERIES_RAW" == "resolute" ]]; then
|
|
||||||
SERIES_LIST=("$UBUNTU_SERIES_RAW")
|
|
||||||
else
|
|
||||||
error "Invalid Ubuntu series: $UBUNTU_SERIES_RAW (use questing, resolute, or omit for both)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
|
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
|
||||||
@@ -138,12 +119,7 @@ elif [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == "all" ]]; then
|
|||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
info "Processing $pkg..."
|
info "Processing $pkg..."
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
BUILD_ARGS=("$pkg")
|
BUILD_ARGS=("$pkg" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
|
||||||
[[ -n "$PPA_NAME_INPUT" ]] && BUILD_ARGS+=("$PPA_NAME_INPUT")
|
|
||||||
if [[ ${#SERIES_LIST[@]} -eq 1 ]]; then
|
|
||||||
BUILD_ARGS+=("${SERIES_LIST[0]}")
|
|
||||||
fi
|
|
||||||
[[ -n "$REBUILD_RELEASE" ]] && BUILD_ARGS+=("$REBUILD_RELEASE")
|
|
||||||
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||||
if ! "$0" "${BUILD_ARGS[@]}"; then
|
if ! "$0" "${BUILD_ARGS[@]}"; then
|
||||||
FAILED_PACKAGES+=("$pkg")
|
FAILED_PACKAGES+=("$pkg")
|
||||||
@@ -189,9 +165,7 @@ else
|
|||||||
|
|
||||||
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
|
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
|
||||||
PACKAGE_INPUT="all"
|
PACKAGE_INPUT="all"
|
||||||
BUILD_ARGS=("all")
|
BUILD_ARGS=("all" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
|
||||||
[[ -n "$PPA_NAME_INPUT" ]] && BUILD_ARGS+=("$PPA_NAME_INPUT")
|
|
||||||
[[ -n "$REBUILD_RELEASE" ]] && BUILD_ARGS+=("$REBUILD_RELEASE")
|
|
||||||
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||||
exec "$0" "${BUILD_ARGS[@]}"
|
exec "$0" "${BUILD_ARGS[@]}"
|
||||||
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
|
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
|
||||||
@@ -217,48 +191,6 @@ fi
|
|||||||
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
||||||
PARENT_DIR=$(dirname "$PACKAGE_DIR")
|
PARENT_DIR=$(dirname "$PACKAGE_DIR")
|
||||||
|
|
||||||
if [[ ${#SERIES_LIST[@]} -gt 1 ]]; then
|
|
||||||
SOURCE_FORMAT_LINE=$(head -1 "$PACKAGE_DIR/debian/source/format" 2>/dev/null || echo "")
|
|
||||||
IS_NATIVE_DUAL=false
|
|
||||||
if [[ "$SOURCE_FORMAT_LINE" == *"native"* ]]; then
|
|
||||||
IS_NATIVE_DUAL=true
|
|
||||||
info "Native source format: second series uses PPA suffix +1 (or ppa2 if unset) so both uploads succeed."
|
|
||||||
fi
|
|
||||||
export REBUILD_RELEASE
|
|
||||||
for idx in "${!SERIES_LIST[@]}"; do
|
|
||||||
SERIES="${SERIES_LIST[$idx]}"
|
|
||||||
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == *"/"* ]]; then
|
|
||||||
ARGS=("$PACKAGE_DIR" "$PPA_NAME" "$SERIES")
|
|
||||||
else
|
|
||||||
ARGS=("$PACKAGE_NAME" "$PPA_NAME" "$SERIES")
|
|
||||||
fi
|
|
||||||
if [[ "$IS_NATIVE_DUAL" == true ]]; then
|
|
||||||
if [[ "$idx" -eq 0 ]]; then
|
|
||||||
[[ -n "${REBUILD_RELEASE:-}" ]] && ARGS+=("$REBUILD_RELEASE")
|
|
||||||
else
|
|
||||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
|
||||||
SECOND_PPA=$((REBUILD_RELEASE + 1))
|
|
||||||
ARGS+=("$SECOND_PPA")
|
|
||||||
info "Second series ${SERIES}: using ppa${SECOND_PPA} (native dual-series)"
|
|
||||||
else
|
|
||||||
ARGS+=("2")
|
|
||||||
info "Second series ${SERIES}: using ppa2 (native dual-series; first uses default ppa1)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
[[ -n "${REBUILD_RELEASE:-}" ]] && ARGS+=("$REBUILD_RELEASE")
|
|
||||||
fi
|
|
||||||
[[ "$KEEP_BUILDS" == "true" ]] && ARGS+=("--keep-builds")
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
info "Upload series: $SERIES (of ${SERIES_LIST[*]})"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
"$0" "${ARGS[@]}" || exit 1
|
|
||||||
done
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
UBUNTU_SERIES="${SERIES_LIST[0]}"
|
|
||||||
|
|
||||||
info "Building and uploading: $PACKAGE_NAME"
|
info "Building and uploading: $PACKAGE_NAME"
|
||||||
info "Package directory: $PACKAGE_DIR"
|
info "Package directory: $PACKAGE_DIR"
|
||||||
info "PPA: ppa:avengemedia/$PPA_NAME"
|
info "PPA: ppa:avengemedia/$PPA_NAME"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Suggests: niri | hyprland | sway
|
Recommends: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -538,8 +538,6 @@ Color picker modal control.
|
|||||||
|
|
||||||
**Functions:**
|
**Functions:**
|
||||||
- `open` - Show color picker modal
|
- `open` - Show color picker modal
|
||||||
- `openColor <color>` - Show color picker modal with a pre-selected color
|
|
||||||
- Parameters: `color` - Color string (e.g. "#ff0000", "#3f51b5")
|
|
||||||
- `close` - Hide color picker modal
|
- `close` - Hide color picker modal
|
||||||
- `closeInstant` - Hide color picker modal without animation
|
- `closeInstant` - Hide color picker modal without animation
|
||||||
- `toggle` - Toggle color picker modal visibility
|
- `toggle` - Toggle color picker modal visibility
|
||||||
|
|||||||
@@ -17,25 +17,6 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
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" ] (
|
||||||
@@ -91,85 +72,76 @@
|
|||||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.lib.makeOverridable (
|
dms-shell = pkgs.buildGoModule (
|
||||||
|
let
|
||||||
|
rootSrc = ./.;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
extraQtPackages ? [ ],
|
inherit version;
|
||||||
}:
|
pname = "dms-shell";
|
||||||
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
|
src = ./core;
|
||||||
let
|
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||||
rootSrc = ./.;
|
|
||||||
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit version;
|
|
||||||
pname = "dms-shell";
|
|
||||||
src = ./core;
|
|
||||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X 'main.Version=${version}'"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
mkdir -p $out/share/quickshell/dms
|
mkdir -p $out/share/quickshell/dms
|
||||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
chmod u+w $out/share/quickshell/dms/VERSION
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
|
|
||||||
# Install desktop file and icon
|
# Install desktop file and icon
|
||||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
$out/share/applications/dms-open.desktop
|
$out/share/applications/dms-open.desktop
|
||||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
wrapProgram $out/bin/dms \
|
wrapProgram $out/bin/dms \
|
||||||
--add-flags "-c $out/share/quickshell/dms" \
|
--add-flags "-c $out/share/quickshell/dms" \
|
||||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
||||||
|
|
||||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
$out/lib/systemd/user/dms.service
|
$out/lib/systemd/user/dms.service
|
||||||
|
|
||||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \
|
installShellCompletion --cmd dms \
|
||||||
--replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.so
|
--bash <($out/bin/dms completion bash) \
|
||||||
|
--fish <($out/bin/dms completion fish) \
|
||||||
|
--zsh <($out/bin/dms completion zsh)
|
||||||
|
'';
|
||||||
|
|
||||||
installShellCompletion --cmd dms \
|
meta = {
|
||||||
--bash <($out/bin/dms completion bash) \
|
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||||
--fish <($out/bin/dms completion fish) \
|
homepage = "https://danklinux.com";
|
||||||
--zsh <($out/bin/dms completion zsh)
|
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||||
'';
|
license = pkgs.lib.licenses.mit;
|
||||||
|
mainProgram = "dms";
|
||||||
meta = {
|
platforms = pkgs.lib.platforms.linux;
|
||||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
};
|
||||||
homepage = "https://danklinux.com";
|
}
|
||||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
);
|
||||||
license = pkgs.lib.licenses.mit;
|
|
||||||
mainProgram = "dms";
|
|
||||||
platforms = pkgs.lib.platforms.linux;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) { };
|
|
||||||
|
|
||||||
quickshell = quickshell.packages.${system}.default;
|
quickshell = quickshell.packages.${system}.default;
|
||||||
|
|
||||||
@@ -209,7 +181,7 @@
|
|||||||
buildInputs =
|
buildInputs =
|
||||||
with pkgs;
|
with pkgs;
|
||||||
[
|
[
|
||||||
(goForPkgs pkgs)
|
go_1_25
|
||||||
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)
|
||||||
make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .`
|
qmllint **/*.qml # Lint all QML files
|
||||||
./qmlformat-all.sh # Format all QML files
|
./qmlformat-all.sh # Format all QML files
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -783,7 +783,7 @@ When modifying the shell:
|
|||||||
|
|
||||||
**QML Frontend:**
|
**QML Frontend:**
|
||||||
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
||||||
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml`, then from repo root run `make lint-qml` after Quickshell has generated the local `.qmlls.ini` VFS with `qs -p .`
|
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml`
|
||||||
3. **Performance**: Ensure animations remain smooth (60 FPS target)
|
3. **Performance**: Ensure animations remain smooth (60 FPS target)
|
||||||
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
||||||
|
|
||||||
|
|||||||
@@ -143,13 +143,8 @@ Singleton {
|
|||||||
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool isDirectionalEffect: isConnectedEffect
|
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
|
||||||
|| (typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1)
|
|
||||||
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
|
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
|
||||||
readonly property bool isConnectedEffect: typeof SettingsData !== "undefined"
|
|
||||||
&& SettingsData.frameEnabled
|
|
||||||
&& SettingsData.motionEffect === 1
|
|
||||||
&& SettingsData.directionalAnimationMode === 3
|
|
||||||
|
|
||||||
readonly property real effectScaleCollapsed: {
|
readonly property real effectScaleCollapsed: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
|
|||||||
@@ -13,13 +13,8 @@ Item {
|
|||||||
|
|
||||||
property color targetColor: "white"
|
property color targetColor: "white"
|
||||||
property real targetRadius: Theme.cornerRadius
|
property real targetRadius: Theme.cornerRadius
|
||||||
property real topLeftRadius: targetRadius
|
|
||||||
property real topRightRadius: targetRadius
|
|
||||||
property real bottomLeftRadius: targetRadius
|
|
||||||
property real bottomRightRadius: targetRadius
|
|
||||||
property color borderColor: "transparent"
|
property color borderColor: "transparent"
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
property bool useCustomSource: false
|
|
||||||
|
|
||||||
property bool shadowEnabled: Theme.elevationEnabled
|
property bool shadowEnabled: Theme.elevationEnabled
|
||||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||||
@@ -51,11 +46,7 @@ Item {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: sourceRect
|
id: sourceRect
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !root.useCustomSource
|
radius: root.targetRadius
|
||||||
topLeftRadius: root.topLeftRadius
|
|
||||||
topRightRadius: root.topRightRadius
|
|
||||||
bottomLeftRadius: root.bottomLeftRadius
|
|
||||||
bottomRightRadius: root.bottomRightRadius
|
|
||||||
color: root.targetColor
|
color: root.targetColor
|
||||||
border.color: root.borderColor
|
border.color: root.borderColor
|
||||||
border.width: root.borderWidth
|
border.width: root.borderWidth
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||||
readonly property url xdgCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
|
||||||
|
|
||||||
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||||
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||||
@@ -73,8 +72,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveIconPath(iconName: string): string {
|
function resolveIconPath(iconName: string): string {
|
||||||
if (!iconName)
|
if (!iconName) return "";
|
||||||
return "";
|
|
||||||
const moddedId = moddedAppId(iconName);
|
const moddedId = moddedAppId(iconName);
|
||||||
if (moddedId !== iconName) {
|
if (moddedId !== iconName) {
|
||||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||||
@@ -87,8 +85,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveIconUrl(iconName: string): string {
|
function resolveIconUrl(iconName: string): string {
|
||||||
if (!iconName)
|
if (!iconName) return "";
|
||||||
return "";
|
|
||||||
const moddedId = moddedAppId(iconName);
|
const moddedId = moddedAppId(iconName);
|
||||||
if (moddedId !== iconName) {
|
if (moddedId !== iconName) {
|
||||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||||
@@ -101,8 +98,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||||
// ! TODO - after QS 0.3, we can install our icon properly
|
if (appId === "org.quickshell") {
|
||||||
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
|
||||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +118,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAppName(appId: string, desktopEntry: var): string {
|
function getAppName(appId: string, desktopEntry: var): string {
|
||||||
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
if (appId === "org.quickshell") {
|
||||||
return "dms";
|
return "dms";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -13,37 +12,6 @@ Singleton {
|
|||||||
signal popoutOpening
|
signal popoutOpening
|
||||||
signal popoutChanged
|
signal popoutChanged
|
||||||
|
|
||||||
function _closePopout(popout) {
|
|
||||||
try {
|
|
||||||
switch (true) {
|
|
||||||
case popout.dashVisible !== undefined:
|
|
||||||
popout.dashVisible = false;
|
|
||||||
return;
|
|
||||||
case popout.notificationHistoryVisible !== undefined:
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
if (typeof popout.close !== "function")
|
|
||||||
return;
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _isStale(popout) {
|
|
||||||
try {
|
|
||||||
if (!popout || !("shouldBeVisible" in popout))
|
|
||||||
return true;
|
|
||||||
if (!popout.screen)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPopout(popout) {
|
function showPopout(popout) {
|
||||||
if (!popout || !popout.screen)
|
if (!popout || !popout.screen)
|
||||||
return;
|
return;
|
||||||
@@ -55,11 +23,13 @@ Singleton {
|
|||||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||||
if (!otherPopout || otherPopout === popout)
|
if (!otherPopout || otherPopout === popout)
|
||||||
continue;
|
continue;
|
||||||
if (_isStale(otherPopout)) {
|
if (otherPopout.dashVisible !== undefined) {
|
||||||
currentPopoutsByScreen[otherScreenName] = null;
|
otherPopout.dashVisible = false;
|
||||||
continue;
|
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
||||||
|
otherPopout.notificationHistoryVisible = false;
|
||||||
|
} else {
|
||||||
|
otherPopout.close();
|
||||||
}
|
}
|
||||||
_closePopout(otherPopout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPopoutsByScreen[screenName] = popout;
|
currentPopoutsByScreen[screenName] = popout;
|
||||||
@@ -81,9 +51,15 @@ Singleton {
|
|||||||
function closeAllPopouts() {
|
function closeAllPopouts() {
|
||||||
for (const screenName in currentPopoutsByScreen) {
|
for (const screenName in currentPopoutsByScreen) {
|
||||||
const popout = currentPopoutsByScreen[screenName];
|
const popout = currentPopoutsByScreen[screenName];
|
||||||
if (!popout || _isStale(popout))
|
if (!popout)
|
||||||
continue;
|
continue;
|
||||||
_closePopout(popout);
|
if (popout.dashVisible !== undefined) {
|
||||||
|
popout.dashVisible = false;
|
||||||
|
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||||
|
popout.notificationHistoryVisible = false;
|
||||||
|
} else {
|
||||||
|
popout.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentPopoutsByScreen = {};
|
currentPopoutsByScreen = {};
|
||||||
}
|
}
|
||||||
@@ -114,12 +90,6 @@ Singleton {
|
|||||||
if (!otherPopout)
|
if (!otherPopout)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (_isStale(otherPopout)) {
|
|
||||||
currentPopoutsByScreen[otherScreenName] = null;
|
|
||||||
currentPopoutTriggers[otherScreenName] = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (otherPopout === popout) {
|
if (otherPopout === popout) {
|
||||||
movedFromOtherScreen = true;
|
movedFromOtherScreen = true;
|
||||||
currentPopoutsByScreen[otherScreenName] = null;
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
@@ -127,26 +97,45 @@ Singleton {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_closePopout(otherPopout);
|
if (otherPopout.dashVisible !== undefined) {
|
||||||
|
otherPopout.dashVisible = false;
|
||||||
|
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
||||||
|
otherPopout.notificationHistoryVisible = false;
|
||||||
|
} else {
|
||||||
|
otherPopout.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout && currentPopout !== popout) {
|
if (currentPopout && currentPopout !== popout) {
|
||||||
if (_isStale(currentPopout)) {
|
if (currentPopout.dashVisible !== undefined) {
|
||||||
currentPopoutsByScreen[screenName] = null;
|
currentPopout.dashVisible = false;
|
||||||
currentPopoutTriggers[screenName] = null;
|
} else if (currentPopout.notificationHistoryVisible !== undefined) {
|
||||||
|
currentPopout.notificationHistoryVisible = false;
|
||||||
} else {
|
} else {
|
||||||
_closePopout(currentPopout);
|
currentPopout.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
||||||
_closePopout(popout);
|
if (popout.dashVisible !== undefined) {
|
||||||
|
popout.dashVisible = false;
|
||||||
|
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||||
|
popout.notificationHistoryVisible = false;
|
||||||
|
} else {
|
||||||
|
popout.close();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerId === undefined) {
|
if (triggerId === undefined) {
|
||||||
_closePopout(popout);
|
if (popout.dashVisible !== undefined) {
|
||||||
|
popout.dashVisible = false;
|
||||||
|
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||||
|
popout.notificationHistoryVisible = false;
|
||||||
|
} else {
|
||||||
|
popout.close();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ Singleton {
|
|||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
property var _loadedSessionSnapshot: null
|
property var _loadedSessionSnapshot: null
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"updateLocale": updateLocale
|
"updateLocale": updateLocale
|
||||||
})
|
})
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -132,12 +132,8 @@ Singleton {
|
|||||||
property string timeLocale: ""
|
property string timeLocale: ""
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
property string launcherLastQuery: ""
|
|
||||||
property var launcherQueryHistory: []
|
|
||||||
property string appDrawerLastMode: "apps"
|
property string appDrawerLastMode: "apps"
|
||||||
property string niriOverviewLastMode: "apps"
|
property string niriOverviewLastMode: "apps"
|
||||||
property string settingsSidebarExpandedIds: ","
|
|
||||||
property string settingsSidebarCollapsedIds: ","
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
@@ -347,8 +343,8 @@ Singleton {
|
|||||||
|
|
||||||
function setLightMode(lightMode) {
|
function setLightMode(lightMode) {
|
||||||
isSwitchingMode = true;
|
isSwitchingMode = true;
|
||||||
syncWallpaperForCurrentMode(lightMode);
|
|
||||||
isLightMode = lightMode;
|
isLightMode = lightMode;
|
||||||
|
syncWallpaperForCurrentMode();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
isSwitchingMode = false;
|
isSwitchingMode = false;
|
||||||
@@ -584,7 +580,14 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
if (!newSettings[identifier]) {
|
||||||
|
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();
|
||||||
@@ -615,7 +618,14 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
if (!newSettings[identifier]) {
|
||||||
|
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();
|
||||||
@@ -646,7 +656,14 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
if (!newSettings[identifier]) {
|
||||||
|
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();
|
||||||
@@ -677,7 +694,14 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
if (!newSettings[identifier]) {
|
||||||
|
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();
|
||||||
@@ -1098,43 +1122,6 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLauncherLastQuery(query) {
|
|
||||||
launcherLastQuery = query;
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLauncherHistory(query) {
|
|
||||||
let q = query.trim();
|
|
||||||
|
|
||||||
setLauncherLastQuery(q);
|
|
||||||
|
|
||||||
if (!q)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (launcherQueryHistory.length > 0 && launcherQueryHistory[0] === q) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let history = [...launcherQueryHistory];
|
|
||||||
|
|
||||||
let idx = history.indexOf(q);
|
|
||||||
if (idx !== -1)
|
|
||||||
history.splice(idx, 1);
|
|
||||||
|
|
||||||
history.unshift(q);
|
|
||||||
if (history.length > 50)
|
|
||||||
history = history.slice(0, 50);
|
|
||||||
|
|
||||||
launcherQueryHistory = history;
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLauncherHistory() {
|
|
||||||
launcherLastQuery = "";
|
|
||||||
launcherSearchHistory = [];
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAppDrawerLastMode(mode) {
|
function setAppDrawerLastMode(mode) {
|
||||||
appDrawerLastMode = mode;
|
appDrawerLastMode = mode;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -1145,22 +1132,15 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSettingsSidebarState(expandedIds, collapsedIds) {
|
function syncWallpaperForCurrentMode() {
|
||||||
settingsSidebarExpandedIds = expandedIds;
|
|
||||||
settingsSidebarCollapsedIds = collapsedIds;
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncWallpaperForCurrentMode(mode) {
|
|
||||||
if (!perModeWallpaper)
|
if (!perModeWallpaper)
|
||||||
return;
|
return;
|
||||||
var light = (mode !== undefined) ? mode : isLightMode;
|
|
||||||
if (perMonitorWallpaper) {
|
if (perMonitorWallpaper) {
|
||||||
monitorWallpapers = light ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
|
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wallpaperPath = light ? wallpaperPathLight : wallpaperPathDark;
|
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _findMonitorValue(map, screenName) {
|
function _findMonitorValue(map, screenName) {
|
||||||
@@ -1238,7 +1218,7 @@ Singleton {
|
|||||||
"time": "06:00"
|
"time": "06:00"
|
||||||
};
|
};
|
||||||
var value = _findMonitorValue(monitorCyclingSettings, screenName);
|
var value = _findMonitorValue(monitorCyclingSettings, screenName);
|
||||||
return Object.assign({}, defaults, value !== undefined ? value : {});
|
return value !== undefined ? value : defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -1265,7 +1245,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
||||||
return greetCfgDir + "/session.json";
|
return greetCfgDir + "/session.json";
|
||||||
}
|
}
|
||||||
preload: isGreeterMode
|
preload: isGreeterMode
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int settingsConfigVersion: 11
|
readonly property int settingsConfigVersion: 6
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
@@ -142,7 +142,6 @@ Singleton {
|
|||||||
property string customThemeFile: ""
|
property string customThemeFile: ""
|
||||||
property var registryThemeVariants: ({})
|
property var registryThemeVariants: ({})
|
||||||
property string matugenScheme: "scheme-tonal-spot"
|
property string matugenScheme: "scheme-tonal-spot"
|
||||||
property real matugenContrast: 0
|
|
||||||
property bool runUserMatugenTemplates: true
|
property bool runUserMatugenTemplates: true
|
||||||
property string matugenTargetMonitor: ""
|
property string matugenTargetMonitor: ""
|
||||||
property real popupTransparency: 1.0
|
property real popupTransparency: 1.0
|
||||||
@@ -163,7 +162,6 @@ 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
|
||||||
@@ -204,48 +202,10 @@ Singleton {
|
|||||||
onPopoutElevationEnabledChanged: saveSettings()
|
onPopoutElevationEnabledChanged: saveSettings()
|
||||||
property bool barElevationEnabled: true
|
property bool barElevationEnabled: true
|
||||||
onBarElevationEnabledChanged: saveSettings()
|
onBarElevationEnabledChanged: saveSettings()
|
||||||
|
|
||||||
property bool blurEnabled: false
|
|
||||||
onBlurEnabledChanged: saveSettings()
|
|
||||||
property string blurBorderColor: "outline"
|
|
||||||
onBlurBorderColorChanged: saveSettings()
|
|
||||||
property string blurBorderCustomColor: "#ffffff"
|
|
||||||
onBlurBorderCustomColorChanged: saveSettings()
|
|
||||||
property real blurBorderOpacity: 1.0
|
|
||||||
onBlurBorderOpacityChanged: saveSettings()
|
|
||||||
property string wallpaperFillMode: "Fill"
|
property string wallpaperFillMode: "Fill"
|
||||||
property bool blurredWallpaperLayer: false
|
property bool blurredWallpaperLayer: false
|
||||||
property bool blurWallpaperOnOverview: false
|
property bool blurWallpaperOnOverview: false
|
||||||
|
|
||||||
property bool frameEnabled: false
|
|
||||||
onFrameEnabledChanged: saveSettings()
|
|
||||||
property real frameThickness: 16
|
|
||||||
onFrameThicknessChanged: saveSettings()
|
|
||||||
property real frameRounding: 23
|
|
||||||
onFrameRoundingChanged: saveSettings()
|
|
||||||
property string frameColor: ""
|
|
||||||
onFrameColorChanged: saveSettings()
|
|
||||||
property real frameOpacity: 1.0
|
|
||||||
onFrameOpacityChanged: saveSettings()
|
|
||||||
property var frameScreenPreferences: ["all"]
|
|
||||||
onFrameScreenPreferencesChanged: saveSettings()
|
|
||||||
property real frameBarSize: 40
|
|
||||||
onFrameBarSizeChanged: saveSettings()
|
|
||||||
property bool frameShowOnOverview: false
|
|
||||||
onFrameShowOnOverviewChanged: saveSettings()
|
|
||||||
property bool frameBlurEnabled: true
|
|
||||||
onFrameBlurEnabledChanged: saveSettings()
|
|
||||||
property int previousDirectionalMode: 1
|
|
||||||
onPreviousDirectionalModeChanged: saveSettings()
|
|
||||||
|
|
||||||
readonly property color effectiveFrameColor: {
|
|
||||||
const fc = frameColor;
|
|
||||||
if (!fc || fc === "default") return Theme.surfaceContainer;
|
|
||||||
if (fc === "primary") return Theme.primary;
|
|
||||||
if (fc === "surface") return Theme.surface;
|
|
||||||
return fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool showLauncherButton: true
|
property bool showLauncherButton: true
|
||||||
property bool showWorkspaceSwitcher: true
|
property bool showWorkspaceSwitcher: true
|
||||||
property bool showFocusedWindow: true
|
property bool showFocusedWindow: true
|
||||||
@@ -338,7 +298,6 @@ Singleton {
|
|||||||
property bool showOccupiedWorkspacesOnly: false
|
property bool showOccupiedWorkspacesOnly: false
|
||||||
property bool reverseScrolling: false
|
property bool reverseScrolling: false
|
||||||
property bool dwlShowAllTags: false
|
property bool dwlShowAllTags: false
|
||||||
property bool workspaceActiveAppHighlightEnabled: false
|
|
||||||
property string workspaceColorMode: "default"
|
property string workspaceColorMode: "default"
|
||||||
property string workspaceOccupiedColorMode: "none"
|
property string workspaceOccupiedColorMode: "none"
|
||||||
property string workspaceUnfocusedColorMode: "default"
|
property string workspaceUnfocusedColorMode: "default"
|
||||||
@@ -372,17 +331,6 @@ Singleton {
|
|||||||
property string centeringMode: "index"
|
property string centeringMode: "index"
|
||||||
property string clockDateFormat: ""
|
property string clockDateFormat: ""
|
||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool greeterRememberLastSession: true
|
|
||||||
property bool greeterRememberLastUser: true
|
|
||||||
property bool greeterEnableFprint: false
|
|
||||||
property bool greeterEnableU2f: false
|
|
||||||
property string greeterWallpaperPath: ""
|
|
||||||
property bool greeterUse24HourClock: true
|
|
||||||
property bool greeterShowSeconds: false
|
|
||||||
property bool greeterPadHours12Hour: false
|
|
||||||
property string greeterLockDateFormat: ""
|
|
||||||
property string greeterFontFamily: ""
|
|
||||||
property string greeterWallpaperFillMode: ""
|
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -394,7 +342,6 @@ Singleton {
|
|||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
property bool rememberLastQuery: false
|
|
||||||
property var spotlightSectionViewModes: ({})
|
property var spotlightSectionViewModes: ({})
|
||||||
onSpotlightSectionViewModesChanged: saveSettings()
|
onSpotlightSectionViewModesChanged: saveSettings()
|
||||||
property var appDrawerSectionViewModes: ({})
|
property var appDrawerSectionViewModes: ({})
|
||||||
@@ -512,11 +459,6 @@ 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
|
||||||
@@ -532,32 +474,18 @@ Singleton {
|
|||||||
property bool matugenTemplateGhostty: true
|
property bool matugenTemplateGhostty: true
|
||||||
property bool matugenTemplateKitty: true
|
property bool matugenTemplateKitty: true
|
||||||
property bool matugenTemplateFoot: true
|
property bool matugenTemplateFoot: true
|
||||||
property bool matugenTemplateNeovim: false
|
property bool matugenTemplateNeovim: true
|
||||||
property bool matugenTemplateAlacritty: true
|
property bool matugenTemplateAlacritty: true
|
||||||
property bool matugenTemplateWezterm: true
|
property bool matugenTemplateWezterm: true
|
||||||
property bool matugenTemplateDgop: true
|
property bool matugenTemplateDgop: true
|
||||||
property bool matugenTemplateKcolorscheme: true
|
property bool matugenTemplateKcolorscheme: true
|
||||||
property bool matugenTemplateVscode: true
|
property bool matugenTemplateVscode: true
|
||||||
property bool matugenTemplateEmacs: true
|
property bool matugenTemplateEmacs: true
|
||||||
property bool matugenTemplateZed: true
|
|
||||||
|
|
||||||
property var matugenTemplateNeovimSettings: ({
|
|
||||||
"dark": {
|
|
||||||
"baseTheme": "github_dark",
|
|
||||||
"harmony": 0.5
|
|
||||||
},
|
|
||||||
"light": {
|
|
||||||
"baseTheme": "github_light",
|
|
||||||
"harmony": 0.5
|
|
||||||
}
|
|
||||||
})
|
|
||||||
property bool matugenTemplateNeovimSetBackground: true
|
|
||||||
|
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
property bool dockSmartAutoHide: false
|
property bool dockSmartAutoHide: false
|
||||||
property bool dockGroupByApp: false
|
property bool dockGroupByApp: false
|
||||||
property bool dockRestoreSpecialWorkspaceOnClick: false
|
|
||||||
property bool dockOpenOnOverview: false
|
property bool dockOpenOnOverview: false
|
||||||
property int dockPosition: SettingsData.Position.Bottom
|
property int dockPosition: SettingsData.Position.Bottom
|
||||||
property real dockSpacing: 4
|
property real dockSpacing: 4
|
||||||
@@ -603,23 +531,9 @@ Singleton {
|
|||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
property bool lockFingerprintCanEnable: false
|
|
||||||
property bool lockFingerprintReady: false
|
|
||||||
property string lockFingerprintReason: "probe_failed"
|
|
||||||
property bool greeterFingerprintCanEnable: false
|
|
||||||
property bool greeterFingerprintReady: false
|
|
||||||
property string greeterFingerprintReason: "probe_failed"
|
|
||||||
property string greeterFingerprintSource: "none"
|
|
||||||
property bool enableU2f: false
|
property 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
|
||||||
@@ -642,7 +556,6 @@ Singleton {
|
|||||||
property bool notificationHistorySaveNormal: true
|
property bool notificationHistorySaveNormal: true
|
||||||
property bool notificationHistorySaveCritical: true
|
property bool notificationHistorySaveCritical: true
|
||||||
property var notificationRules: []
|
property var notificationRules: []
|
||||||
property bool notificationFocusedMonitor: false
|
|
||||||
|
|
||||||
property bool osdAlwaysShowValue: false
|
property bool osdAlwaysShowValue: false
|
||||||
property int osdPosition: SettingsData.Position.BottomCenter
|
property int osdPosition: SettingsData.Position.BottomCenter
|
||||||
@@ -1106,19 +1019,13 @@ Singleton {
|
|||||||
signal widgetDataChanged
|
signal widgetDataChanged
|
||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
function refreshAuthAvailability() {
|
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Processes.settingsRoot = root;
|
|
||||||
Processes.detectAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
refreshAuthAvailability();
|
Processes.detectFprintd();
|
||||||
|
Processes.detectU2f();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1260,15 +1167,6 @@ Singleton {
|
|||||||
Quickshell.execDetached(["sh", "-lc", script]);
|
Quickshell.execDetached(["sh", "-lc", script]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
Processes.settingsRoot = root;
|
|
||||||
Processes.scheduleAuthApply();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"applyStoredTheme": applyStoredTheme,
|
"applyStoredTheme": applyStoredTheme,
|
||||||
"regenSystemThemes": regenSystemThemes,
|
"regenSystemThemes": regenSystemThemes,
|
||||||
@@ -1276,7 +1174,6 @@ Singleton {
|
|||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor,
|
||||||
"scheduleAuthApply": scheduleAuthApply
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
@@ -1368,45 +1265,10 @@ Singleton {
|
|||||||
return JSON.stringify(Store.toJson(root), null, 2);
|
return JSON.stringify(Store.toJson(root), null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resetPluginSettings() {
|
|
||||||
_pluginParseError = false;
|
|
||||||
pluginSettings = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function _pluginSettingsErrorCode(error) {
|
|
||||||
if (typeof error === "number")
|
|
||||||
return error;
|
|
||||||
if (error && typeof error === "object") {
|
|
||||||
if (typeof error.code === "number")
|
|
||||||
return error.code;
|
|
||||||
if (typeof error.errno === "number")
|
|
||||||
return error.errno;
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = String(error || "").trim();
|
|
||||||
if (/^\d+$/.test(msg))
|
|
||||||
return Number(msg);
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _isMissingPluginSettingsError(error) {
|
|
||||||
if (_pluginSettingsErrorCode(error) === 2)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const msg = String(error || "").toLowerCase();
|
|
||||||
return msg.indexOf("file does not exist") !== -1 || msg.indexOf("no such file") !== -1 || msg.indexOf("enoent") !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPluginSettings() {
|
function loadPluginSettings() {
|
||||||
try {
|
_pluginSettingsLoading = true;
|
||||||
parsePluginSettings(pluginSettingsFile.text());
|
parsePluginSettings(pluginSettingsFile.text());
|
||||||
} catch (e) {
|
_pluginSettingsLoading = false;
|
||||||
const msg = e.message || String(e);
|
|
||||||
if (!_isMissingPluginSettingsError(e))
|
|
||||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
|
||||||
_resetPluginSettings();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePluginSettings(content) {
|
function parsePluginSettings(content) {
|
||||||
@@ -1591,37 +1453,35 @@ Singleton {
|
|||||||
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
|
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
|
||||||
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
||||||
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||||
const isConnected = frameEnabled && motionEffect === 1 && directionalAnimationMode === 3;
|
const bottomGap = Math.max(0, rawBottomGap);
|
||||||
const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap);
|
|
||||||
|
|
||||||
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
|
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
|
||||||
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
|
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
|
||||||
const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue);
|
const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue;
|
||||||
const edgeSpacing = isConnected ? 0 : spacing;
|
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case SettingsData.Position.Left:
|
case SettingsData.Position.Left:
|
||||||
return {
|
return {
|
||||||
"x": barThickness + edgeSpacing + popupGap,
|
"x": barThickness + spacing + popupGap,
|
||||||
"y": relativeY,
|
"y": relativeY,
|
||||||
"width": widgetWidth
|
"width": widgetWidth
|
||||||
};
|
};
|
||||||
case SettingsData.Position.Right:
|
case SettingsData.Position.Right:
|
||||||
return {
|
return {
|
||||||
"x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap),
|
"x": (screen?.width || 0) - (barThickness + spacing + popupGap),
|
||||||
"y": relativeY,
|
"y": relativeY,
|
||||||
"width": widgetWidth
|
"width": widgetWidth
|
||||||
};
|
};
|
||||||
case SettingsData.Position.Bottom:
|
case SettingsData.Position.Bottom:
|
||||||
return {
|
return {
|
||||||
"x": relativeX,
|
"x": relativeX,
|
||||||
"y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap),
|
"y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap),
|
||||||
"width": widgetWidth
|
"width": widgetWidth
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
"x": relativeX,
|
"x": relativeX,
|
||||||
"y": barThickness + edgeSpacing + bottomGap + popupGap,
|
"y": barThickness + spacing + bottomGap + popupGap,
|
||||||
"width": widgetWidth
|
"width": widgetWidth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1715,9 +1575,7 @@ Singleton {
|
|||||||
const screenWidth = screen.width;
|
const screenWidth = screen.width;
|
||||||
const screenHeight = screen.height;
|
const screenHeight = screen.height;
|
||||||
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
||||||
const isConnected = frameEnabled && motionEffect === 1 && directionalAnimationMode === 3;
|
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||||
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
|
||||||
const bottomGap = isConnected ? 0 : rawBottomGap;
|
|
||||||
|
|
||||||
let topOffset = 0;
|
let topOffset = 0;
|
||||||
let bottomOffset = 0;
|
let bottomOffset = 0;
|
||||||
@@ -1739,7 +1597,7 @@ Singleton {
|
|||||||
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
|
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
|
||||||
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
|
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
|
||||||
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
|
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
|
||||||
const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0));
|
const otherBottomGap = other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0);
|
||||||
|
|
||||||
switch (other.position) {
|
switch (other.position) {
|
||||||
case SettingsData.Position.Top:
|
case SettingsData.Position.Top:
|
||||||
@@ -1990,66 +1848,6 @@ Singleton {
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFrameFilteredScreens() {
|
|
||||||
var prefs = frameScreenPreferences || ["all"];
|
|
||||||
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
|
|
||||||
return Quickshell.screens;
|
|
||||||
}
|
|
||||||
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveBarEdgeForScreen(screen) {
|
|
||||||
if (!screen) return "";
|
|
||||||
for (var i = 0; i < barConfigs.length; i++) {
|
|
||||||
var bc = barConfigs[i];
|
|
||||||
if (!bc.enabled) continue;
|
|
||||||
var prefs = bc.screenPreferences || ["all"];
|
|
||||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
|
||||||
switch (bc.position ?? 0) {
|
|
||||||
case SettingsData.Position.Top: return "top";
|
|
||||||
case SettingsData.Position.Bottom: return "bottom";
|
|
||||||
case SettingsData.Position.Left: return "left";
|
|
||||||
case SettingsData.Position.Right: return "right";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveBarEdgesForScreen(screen) {
|
|
||||||
if (!screen) return [];
|
|
||||||
var edges = [];
|
|
||||||
for (var i = 0; i < barConfigs.length; i++) {
|
|
||||||
var bc = barConfigs[i];
|
|
||||||
if (!bc.enabled) continue;
|
|
||||||
var prefs = bc.screenPreferences || ["all"];
|
|
||||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
|
||||||
switch (bc.position ?? 0) {
|
|
||||||
case SettingsData.Position.Top: edges.push("top"); break;
|
|
||||||
case SettingsData.Position.Bottom: edges.push("bottom"); break;
|
|
||||||
case SettingsData.Position.Left: edges.push("left"); break;
|
|
||||||
case SettingsData.Position.Right: edges.push("right"); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveBarThicknessForScreen(screen) {
|
|
||||||
if (frameEnabled) return frameBarSize;
|
|
||||||
if (!screen) return frameThickness;
|
|
||||||
for (var i = 0; i < barConfigs.length; i++) {
|
|
||||||
var bc = barConfigs[i];
|
|
||||||
if (!bc.enabled) continue;
|
|
||||||
var prefs = bc.screenPreferences || ["all"];
|
|
||||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
|
||||||
const innerPadding = bc.innerPadding ?? 4;
|
|
||||||
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
|
||||||
const spacing = bc.spacing ?? 4;
|
|
||||||
const bottomGap = bc.bottomGap ?? 0;
|
|
||||||
return barT + spacing + bottomGap;
|
|
||||||
}
|
|
||||||
return frameThickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendTestNotifications() {
|
function sendTestNotifications() {
|
||||||
NotificationService.dismissAllPopups();
|
NotificationService.dismissAllPopups();
|
||||||
sendTestNotification(0);
|
sendTestNotification(0);
|
||||||
@@ -2079,12 +1877,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMatugenContrast(value) {
|
|
||||||
if (matugenContrast === value)
|
|
||||||
return;
|
|
||||||
set("matugenContrast", value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRunUserMatugenTemplates(enabled) {
|
function setRunUserMatugenTemplates(enabled) {
|
||||||
if (runUserMatugenTemplates === enabled)
|
if (runUserMatugenTemplates === enabled)
|
||||||
return;
|
return;
|
||||||
@@ -2912,7 +2704,6 @@ Singleton {
|
|||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
printErrors: false
|
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: !isGreeterMode
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
@@ -2921,10 +2712,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
const msg = String(error || "");
|
pluginSettings = {};
|
||||||
if (!_isMissingPluginSettingsError(error))
|
|
||||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
|
||||||
_resetPluginSettings();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -972,22 +972,6 @@ Singleton {
|
|||||||
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
|
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
|
||||||
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
|
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
|
||||||
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
||||||
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
|
|
||||||
readonly property real connectedCornerRadius: {
|
|
||||||
if (typeof SettingsData === "undefined") return 12;
|
|
||||||
return SettingsData.frameEnabled ? SettingsData.frameRounding : cornerRadius;
|
|
||||||
}
|
|
||||||
readonly property color connectedSurfaceColor: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return withAlpha(surfaceContainer, popupTransparency);
|
|
||||||
return isConnectedEffect
|
|
||||||
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
|
||||||
: withAlpha(surfaceContainer, popupTransparency);
|
|
||||||
}
|
|
||||||
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
|
||||||
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined")
|
|
||||||
? true
|
|
||||||
: (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
|
||||||
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
||||||
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
||||||
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
|
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
|
||||||
@@ -1118,7 +1102,7 @@ Singleton {
|
|||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: {
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||||
return GreetdSettings.getEffectiveFontFamily();
|
return GreetdSettings.fontFamily;
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -1159,13 +1143,7 @@ Singleton {
|
|||||||
property real iconSizeLarge: 32
|
property real iconSizeLarge: 32
|
||||||
|
|
||||||
property real panelTransparency: 0.85
|
property real panelTransparency: 0.85
|
||||||
property real popupTransparency: {
|
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 1.0;
|
|
||||||
if (isConnectedEffect)
|
|
||||||
return SettingsData.frameOpacity !== undefined ? SettingsData.frameOpacity : 1.0;
|
|
||||||
return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function screenTransition() {
|
function screenTransition() {
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
@@ -1288,8 +1266,7 @@ Singleton {
|
|||||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||||
const defaults = themeData.variants.defaults || {};
|
const defaults = themeData.variants.defaults || {};
|
||||||
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
|
||||||
const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
|
|
||||||
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
||||||
const accentId = stored.accent || modeDefaults.accent || "";
|
const accentId = stored.accent || modeDefaults.accent || "";
|
||||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||||
@@ -1315,8 +1292,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
|
||||||
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
|
|
||||||
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||||
@@ -1589,14 +1565,11 @@ Singleton {
|
|||||||
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
||||||
args.push("--terminals-always-dark");
|
args.push("--terminals-always-dark");
|
||||||
}
|
}
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.matugenContrast !== 0) {
|
|
||||||
args.push("--contrast", SettingsData.matugenContrast.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
const skipTemplates = [];
|
const skipTemplates = [];
|
||||||
if (!SettingsData.runDmsMatugenTemplates) {
|
if (!SettingsData.runDmsMatugenTemplates) {
|
||||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
|
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
|
||||||
} else {
|
} else {
|
||||||
if (!SettingsData.matugenTemplateGtk)
|
if (!SettingsData.matugenTemplateGtk)
|
||||||
skipTemplates.push("gtk");
|
skipTemplates.push("gtk");
|
||||||
@@ -1640,8 +1613,6 @@ Singleton {
|
|||||||
skipTemplates.push("vscode");
|
skipTemplates.push("vscode");
|
||||||
if (!SettingsData.matugenTemplateEmacs)
|
if (!SettingsData.matugenTemplateEmacs)
|
||||||
skipTemplates.push("emacs");
|
skipTemplates.push("emacs");
|
||||||
if (!SettingsData.matugenTemplateZed)
|
|
||||||
skipTemplates.push("zed");
|
|
||||||
}
|
}
|
||||||
if (skipTemplates.length > 0) {
|
if (skipTemplates.length > 0) {
|
||||||
args.push("--skip-templates", skipTemplates.join(","));
|
args.push("--skip-templates", skipTemplates.join(","));
|
||||||
@@ -1691,9 +1662,8 @@ Singleton {
|
|||||||
const defaults = customThemeRawData.variants.defaults || {};
|
const defaults = customThemeRawData.variants.defaults || {};
|
||||||
const darkDefaults = defaults.dark || {};
|
const darkDefaults = defaults.dark || {};
|
||||||
const lightDefaults = defaults.light || defaults.dark || {};
|
const lightDefaults = defaults.light || defaults.dark || {};
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
||||||
const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
|
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
||||||
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
|
||||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
const accentId = storedDark.accent || darkDefaults.accent || "";
|
||||||
@@ -1711,8 +1681,7 @@ Singleton {
|
|||||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
||||||
}
|
}
|
||||||
} else if (customThemeRawData.variants.options) {
|
} else if (customThemeRawData.variants.options) {
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
|
||||||
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
|
|
||||||
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||||
@@ -1864,12 +1833,6 @@ Singleton {
|
|||||||
return Qt.rgba(c.r, c.g, c.b, a);
|
return Qt.rgba(c.r, c.g, c.b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
function popupLayerColor(baseColor) {
|
|
||||||
if (isConnectedEffect)
|
|
||||||
return connectedSurfaceColor;
|
|
||||||
return withAlpha(baseColor, popupTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
function blendAlpha(c, a) {
|
function blendAlpha(c, a) {
|
||||||
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
||||||
}
|
}
|
||||||
@@ -2042,11 +2005,10 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
blockLoading: false
|
|
||||||
watchChanges: !SessionData.isGreeterMode
|
watchChanges: !SessionData.isGreeterMode
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
function parseAndLoadColors() {
|
||||||
|
|||||||
@@ -1249,7 +1249,7 @@ const defaultOpts = {
|
|||||||
};
|
};
|
||||||
class Finder {
|
class Finder {
|
||||||
constructor(list, ...optionsTuple) {
|
constructor(list, ...optionsTuple) {
|
||||||
this.opts = Object.assign({}, defaultOpts, optionsTuple[0]);
|
this.opts = Object.assign(defaultOpts, optionsTuple[0]);
|
||||||
this.items = list;
|
this.items = list;
|
||||||
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
|
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
|
||||||
this.algoFn = exactMatchNaive;
|
this.algoFn = exactMatchNaive;
|
||||||
@@ -1283,13 +1283,12 @@ function postProcessResultItems(result, opts) {
|
|||||||
if (opts.sort) {
|
if (opts.sort) {
|
||||||
const { selector } = opts;
|
const { selector } = opts;
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
if (a.score !== b.score) {
|
if (a.score === b.score) {
|
||||||
return b.score - a.score;
|
for (const tiebreaker of opts.tiebreakers) {
|
||||||
}
|
const diff = tiebreaker(a, b, selector);
|
||||||
for (const tiebreaker of opts.tiebreakers) {
|
if (diff !== 0) {
|
||||||
const diff = tiebreaker(a, b, selector);
|
return diff;
|
||||||
if (diff !== 0) {
|
}
|
||||||
return diff;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -4,410 +4,28 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
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")
|
|
||||||
property bool authApplyRunning: false
|
|
||||||
property bool authApplyQueued: false
|
|
||||||
property bool authApplyRerunRequested: false
|
|
||||||
property bool authApplyTerminalFallbackFromPrecheck: false
|
|
||||||
property string authApplyStdout: ""
|
|
||||||
property string authApplyStderr: ""
|
|
||||||
property string authApplySudoProbeStderr: ""
|
|
||||||
property string authApplyTerminalFallbackStderr: ""
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
pamFprintSupportDetected = false;
|
|
||||||
pamU2fSupportDetected = false;
|
|
||||||
pamSupportProbeOutput = "";
|
|
||||||
pamSupportProbeStreamFinished = false;
|
|
||||||
pamSupportProbeExited = false;
|
|
||||||
pamSupportDetectionProcess.running = true;
|
|
||||||
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectFprintd() {
|
function detectFprintd() {
|
||||||
detectAuthCapabilities();
|
fprintdDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectU2f() {
|
function detectU2f() {
|
||||||
detectAuthCapabilities();
|
u2fDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
function checkPluginSettings() {
|
||||||
pluginSettingsCheckProcess.running = true;
|
pluginSettingsCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
authApplyQueued = true;
|
|
||||||
if (authApplyRunning) {
|
|
||||||
authApplyRerunRequested = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
authApplyDebounce.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
function beginAuthApply() {
|
|
||||||
if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
authApplyQueued = false;
|
|
||||||
authApplyRerunRequested = false;
|
|
||||||
authApplyStdout = "";
|
|
||||||
authApplyStderr = "";
|
|
||||||
authApplySudoProbeStderr = "";
|
|
||||||
authApplyTerminalFallbackStderr = "";
|
|
||||||
authApplyTerminalFallbackFromPrecheck = false;
|
|
||||||
authApplyRunning = true;
|
|
||||||
authApplySudoProbeProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchAuthApplyTerminalFallback(fromPrecheck, details) {
|
|
||||||
authApplyTerminalFallbackFromPrecheck = fromPrecheck;
|
|
||||||
if (details && details !== "")
|
|
||||||
ToastService.showInfo(I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync");
|
|
||||||
authApplyTerminalFallbackStderr = "";
|
|
||||||
authApplyTerminalFallbackProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishAuthApply() {
|
|
||||||
const shouldRerun = authApplyQueued || authApplyRerunRequested;
|
|
||||||
authApplyRunning = false;
|
|
||||||
authApplyRerunRequested = false;
|
|
||||||
if (shouldRerun)
|
|
||||||
authApplyDebounce.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -417,15 +35,15 @@ Singleton {
|
|||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
const lines = text.trim().split("\n");
|
var lines = text.trim().split('\n');
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (var i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
var line = lines[i];
|
||||||
if (line.startsWith("qt5ct:")) {
|
if (line.startsWith('qt5ct:')) {
|
||||||
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
|
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
|
||||||
} else if (line.startsWith("qt6ct:")) {
|
} else if (line.startsWith('qt6ct:')) {
|
||||||
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
|
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
|
||||||
} else if (line.startsWith("gtk:")) {
|
} else if (line.startsWith('gtk:')) {
|
||||||
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
|
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,266 +51,23 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var fingerprintProbeProcess: Process {
|
property var fprintdDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.fingerprintProbeOutput = text || "";
|
|
||||||
root.fingerprintProbeStreamFinished = true;
|
|
||||||
root.finalizeFingerprintProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
root.fingerprintProbeExitCode = exitCode;
|
if (!settingsRoot)
|
||||||
root.fingerprintProbeExited = true;
|
return;
|
||||||
root.finalizeFingerprintProbe();
|
settingsRoot.fprintdAvailable = (exitCode === 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var pamSupportDetectionProcess: Process {
|
property var u2fDetectionProcess: 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"]
|
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
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.pamSupportProbeOutput = text || "";
|
|
||||||
root.pamSupportProbeStreamFinished = true;
|
|
||||||
root.finalizePamSupportProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
root.pamSupportProbeExitCode = exitCode;
|
if (!settingsRoot)
|
||||||
root.pamSupportProbeExited = true;
|
|
||||||
root.finalizePamSupportProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: authApplyDebounce
|
|
||||||
interval: 300
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.beginAuthApply()
|
|
||||||
}
|
|
||||||
|
|
||||||
property var authApplyProcess: Process {
|
|
||||||
command: ["dms", "auth", "sync", "--yes"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: root.authApplyStdout = text || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: root.authApplyStderr = text || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
const out = (root.authApplyStdout || "").trim();
|
|
||||||
const err = (root.authApplyStderr || "").trim();
|
|
||||||
|
|
||||||
if (exitCode === 0) {
|
|
||||||
let details = out;
|
|
||||||
if (err !== "")
|
|
||||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
|
||||||
ToastService.showInfo(I18n.tr("Authentication changes applied."), details, "", "auth-sync");
|
|
||||||
root.detectAuthCapabilities();
|
|
||||||
root.finishAuthApply();
|
|
||||||
return;
|
return;
|
||||||
}
|
settingsRoot.u2fAvailable = (exitCode === 0);
|
||||||
|
|
||||||
let details = "";
|
|
||||||
if (out !== "")
|
|
||||||
details = out;
|
|
||||||
if (err !== "")
|
|
||||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
|
||||||
ToastService.showWarning(I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync");
|
|
||||||
root.launchAuthApplyTerminalFallback(false, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var authApplySudoProbeProcess: Process {
|
|
||||||
command: ["sudo", "-n", "true"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: root.authApplySudoProbeStderr = text || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
const err = (root.authApplySudoProbeStderr || "").trim();
|
|
||||||
if (exitCode === 0) {
|
|
||||||
ToastService.showInfo(I18n.tr("Applying authentication changes…"), "", "", "auth-sync");
|
|
||||||
root.authApplyProcess.running = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.launchAuthApplyTerminalFallback(true, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var authApplyTerminalFallbackProcess: Process {
|
|
||||||
command: ["dms", "auth", "sync", "--terminal", "--yes"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: root.authApplyTerminalFallbackStderr = text || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
const message = root.authApplyTerminalFallbackFromPrecheck
|
|
||||||
? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.")
|
|
||||||
: I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
|
|
||||||
ToastService.showInfo(message, "", "", "auth-sync");
|
|
||||||
} else {
|
|
||||||
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
|
||||||
ToastService.showError(I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
|
|
||||||
}
|
|
||||||
root.finishAuthApply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,13 +83,8 @@ var SPEC = {
|
|||||||
timeLocale: { def: "" },
|
timeLocale: { def: "" },
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
launcherLastQuery: { def: "" },
|
|
||||||
launcherQueryHistory: { def: [] },
|
|
||||||
appDrawerLastMode: { def: "apps" },
|
appDrawerLastMode: { def: "apps" },
|
||||||
niriOverviewLastMode: { def: "apps" },
|
niriOverviewLastMode: { def: "apps" }
|
||||||
|
|
||||||
settingsSidebarExpandedIds: { def: "," },
|
|
||||||
settingsSidebarCollapsedIds: { def: "," }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ var SPEC = {
|
|||||||
customThemeFile: { def: "" },
|
customThemeFile: { def: "" },
|
||||||
registryThemeVariants: { def: {} },
|
registryThemeVariants: { def: {} },
|
||||||
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
|
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
|
||||||
matugenContrast: { def: 0, onChange: "regenSystemThemes" },
|
|
||||||
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
|
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
|
||||||
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
|
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ 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 },
|
||||||
@@ -52,7 +50,6 @@ var SPEC = {
|
|||||||
animationVariant: { def: 0 },
|
animationVariant: { def: 0 },
|
||||||
motionEffect: { def: 0 },
|
motionEffect: { def: 0 },
|
||||||
directionalAnimationMode: { def: 0 },
|
directionalAnimationMode: { def: 0 },
|
||||||
previousDirectionalMode: { def: 1 },
|
|
||||||
m3ElevationEnabled: { def: true },
|
m3ElevationEnabled: { def: true },
|
||||||
m3ElevationIntensity: { def: 12 },
|
m3ElevationIntensity: { def: 12 },
|
||||||
m3ElevationOpacity: { def: 30 },
|
m3ElevationOpacity: { def: 30 },
|
||||||
@@ -62,10 +59,6 @@ var SPEC = {
|
|||||||
modalElevationEnabled: { def: true },
|
modalElevationEnabled: { def: true },
|
||||||
popoutElevationEnabled: { def: true },
|
popoutElevationEnabled: { def: true },
|
||||||
barElevationEnabled: { def: true },
|
barElevationEnabled: { def: true },
|
||||||
blurEnabled: { def: false },
|
|
||||||
blurBorderColor: { def: "outline" },
|
|
||||||
blurBorderCustomColor: { def: "#ffffff" },
|
|
||||||
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
|
||||||
wallpaperFillMode: { def: "Fill" },
|
wallpaperFillMode: { def: "Fill" },
|
||||||
blurredWallpaperLayer: { def: false },
|
blurredWallpaperLayer: { def: false },
|
||||||
blurWallpaperOnOverview: { def: false },
|
blurWallpaperOnOverview: { def: false },
|
||||||
@@ -133,7 +126,6 @@ var SPEC = {
|
|||||||
showOccupiedWorkspacesOnly: { def: false },
|
showOccupiedWorkspacesOnly: { def: false },
|
||||||
reverseScrolling: { def: false },
|
reverseScrolling: { def: false },
|
||||||
dwlShowAllTags: { def: false },
|
dwlShowAllTags: { def: false },
|
||||||
workspaceActiveAppHighlightEnabled: { def: false },
|
|
||||||
workspaceColorMode: { def: "default" },
|
workspaceColorMode: { def: "default" },
|
||||||
workspaceOccupiedColorMode: { def: "none" },
|
workspaceOccupiedColorMode: { def: "none" },
|
||||||
workspaceUnfocusedColorMode: { def: "default" },
|
workspaceUnfocusedColorMode: { def: "default" },
|
||||||
@@ -175,17 +167,6 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
greeterRememberLastSession: { def: true },
|
|
||||||
greeterRememberLastUser: { def: true },
|
|
||||||
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
|
||||||
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
|
||||||
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" },
|
||||||
@@ -197,7 +178,6 @@ var SPEC = {
|
|||||||
sortAppsAlphabetically: { def: false },
|
sortAppsAlphabetically: { def: false },
|
||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
rememberLastQuery: { def: false },
|
|
||||||
spotlightSectionViewModes: { def: {} },
|
spotlightSectionViewModes: { def: {} },
|
||||||
appDrawerSectionViewModes: { def: {} },
|
appDrawerSectionViewModes: { def: {} },
|
||||||
niriOverviewOverlayEnabled: { def: true },
|
niriOverviewOverlayEnabled: { def: true },
|
||||||
@@ -279,11 +259,6 @@ 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 },
|
||||||
@@ -300,27 +275,17 @@ var SPEC = {
|
|||||||
matugenTemplateKitty: { def: true },
|
matugenTemplateKitty: { def: true },
|
||||||
matugenTemplateFoot: { def: true },
|
matugenTemplateFoot: { def: true },
|
||||||
matugenTemplateAlacritty: { def: true },
|
matugenTemplateAlacritty: { def: true },
|
||||||
matugenTemplateNeovim: { def: false },
|
matugenTemplateNeovim: { def: true },
|
||||||
matugenTemplateWezterm: { def: true },
|
matugenTemplateWezterm: { def: true },
|
||||||
matugenTemplateDgop: { def: true },
|
matugenTemplateDgop: { def: true },
|
||||||
matugenTemplateKcolorscheme: { def: true },
|
matugenTemplateKcolorscheme: { def: true },
|
||||||
matugenTemplateVscode: { def: true },
|
matugenTemplateVscode: { def: true },
|
||||||
matugenTemplateEmacs: { def: true },
|
matugenTemplateEmacs: { def: true },
|
||||||
matugenTemplateZed: { def: true },
|
|
||||||
|
|
||||||
matugenTemplateNeovimSettings: {
|
|
||||||
def: {
|
|
||||||
dark: { baseTheme: "github_dark", harmony: 0.5 },
|
|
||||||
light: { baseTheme: "github_light", harmony: 0.5 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
matugenTemplateNeovimSetBackground: { def: true },
|
|
||||||
|
|
||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
dockSmartAutoHide: { def: false },
|
dockSmartAutoHide: { def: false },
|
||||||
dockGroupByApp: { def: false },
|
dockGroupByApp: { def: false },
|
||||||
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
|
||||||
dockOpenOnOverview: { def: false },
|
dockOpenOnOverview: { def: false },
|
||||||
dockPosition: { def: 1 },
|
dockPosition: { def: 1 },
|
||||||
dockSpacing: { def: 4 },
|
dockSpacing: { def: 4 },
|
||||||
@@ -362,26 +327,12 @@ var SPEC = {
|
|||||||
lockScreenShowMediaPlayer: { def: true },
|
lockScreenShowMediaPlayer: { def: true },
|
||||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
lockAtStartup: { def: false },
|
lockAtStartup: { def: false },
|
||||||
enableFprint: { def: false, onChange: "scheduleAuthApply" },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
lockFingerprintCanEnable: { def: false, persist: false },
|
enableU2f: { def: 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, onChange: "scheduleAuthApply" },
|
|
||||||
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 },
|
||||||
@@ -404,7 +355,6 @@ var SPEC = {
|
|||||||
notificationHistorySaveNormal: { def: true },
|
notificationHistorySaveNormal: { def: true },
|
||||||
notificationHistorySaveCritical: { def: true },
|
notificationHistorySaveCritical: { def: true },
|
||||||
notificationRules: { def: [] },
|
notificationRules: { def: [] },
|
||||||
notificationFocusedMonitor: { def: false },
|
|
||||||
|
|
||||||
osdAlwaysShowValue: { def: false },
|
osdAlwaysShowValue: { def: false },
|
||||||
osdPosition: { def: 5 },
|
osdPosition: { def: 5 },
|
||||||
@@ -551,17 +501,7 @@ var SPEC = {
|
|||||||
clipboardEnterToPaste: { def: false },
|
clipboardEnterToPaste: { def: false },
|
||||||
|
|
||||||
launcherPluginVisibility: { def: {} },
|
launcherPluginVisibility: { def: {} },
|
||||||
launcherPluginOrder: { def: [] },
|
launcherPluginOrder: { def: [] }
|
||||||
|
|
||||||
frameEnabled: { def: false },
|
|
||||||
frameThickness: { def: 16 },
|
|
||||||
frameRounding: { def: 23 },
|
|
||||||
frameColor: { def: "" },
|
|
||||||
frameOpacity: { def: 1.0 },
|
|
||||||
frameScreenPreferences: { def: ["all"] },
|
|
||||||
frameBarSize: { def: 40 },
|
|
||||||
frameShowOnOverview: { def: false },
|
|
||||||
frameBlurEnabled: { def: true }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -248,10 +248,6 @@ function migrateToVersion(obj, targetVersion) {
|
|||||||
settings.configVersion = 6;
|
settings.configVersion = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentVersion < 11) {
|
|
||||||
settings.configVersion = 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Services.Greetd
|
||||||
|
import qs.Common
|
||||||
import qs.Modules.Greetd
|
import qs.Modules.Greetd
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
|
|||||||
+4
-10
@@ -21,7 +21,6 @@ import qs.Modules.OSD
|
|||||||
import qs.Modules.ProcessList
|
import qs.Modules.ProcessList
|
||||||
import qs.Modules.DankBar
|
import qs.Modules.DankBar
|
||||||
import qs.Modules.DankBar.Popouts
|
import qs.Modules.DankBar.Popouts
|
||||||
import qs.Modules.Frame
|
|
||||||
import qs.Modules.WorkspaceOverlays
|
import qs.Modules.WorkspaceOverlays
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
@@ -177,8 +176,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Frame {}
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: dankBarRepeater
|
id: dankBarRepeater
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
@@ -316,7 +313,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
|
model: SettingsData.getFilteredScreens("notifications")
|
||||||
|
|
||||||
delegate: NotificationPopupManager {
|
delegate: NotificationPopupManager {
|
||||||
modelData: item
|
modelData: item
|
||||||
@@ -622,10 +619,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MuxModal {
|
|
||||||
id: muxModal
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipboardHistoryModal {
|
ClipboardHistoryModal {
|
||||||
id: clipboardHistoryModalPopup
|
id: clipboardHistoryModalPopup
|
||||||
|
|
||||||
@@ -822,8 +815,9 @@ Item {
|
|||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Notepad {
|
Notepad {
|
||||||
slideout: notepadSlideout
|
onHideRequested: {
|
||||||
onHideRequested: notepadSlideout.hide()
|
notepadSlideout.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ Rectangle {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: keyboardHints.enterToPaste
|
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||||
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
|
|
||||||
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Quickshell
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -35,12 +34,6 @@ Item {
|
|||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
|
||||||
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
|
|
||||||
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
|
||||||
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
|
||||||
readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius
|
|
||||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
|
||||||
property bool enableShadow: true
|
property bool enableShadow: true
|
||||||
property alias modalFocusScope: focusScope
|
property alias modalFocusScope: focusScope
|
||||||
property bool shouldBeVisible: false
|
property bool shouldBeVisible: false
|
||||||
@@ -170,8 +163,6 @@ Item {
|
|||||||
readonly property real shadowFallbackOffset: 6
|
readonly property real shadowFallbackOffset: 6
|
||||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||||
readonly property real shadowMotionPadding: {
|
readonly property real shadowMotionPadding: {
|
||||||
if (Theme.isConnectedEffect)
|
|
||||||
return 0;
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
|
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
|
||||||
return 0; // Wayland native overlap mask
|
return 0; // Wayland native overlap mask
|
||||||
if (animationType === "slide")
|
if (animationType === "slide")
|
||||||
@@ -253,7 +244,7 @@ Item {
|
|||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
@@ -268,17 +259,6 @@ Item {
|
|||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
WindowBlur {
|
|
||||||
targetWindow: contentWindow
|
|
||||||
blurEnabled: root.effectiveBlurEnabled
|
|
||||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
|
||||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
|
||||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
|
||||||
blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
|
|
||||||
blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
|
|
||||||
blurRadius: root.effectiveCornerRadius
|
|
||||||
}
|
|
||||||
|
|
||||||
WlrLayershell.namespace: root.layerNamespace
|
WlrLayershell.namespace: root.layerNamespace
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: {
|
||||||
if (root.useOverlayLayer)
|
if (root.useOverlayLayer)
|
||||||
@@ -353,7 +333,7 @@ Item {
|
|||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
@@ -507,12 +487,12 @@ Item {
|
|||||||
id: animatedContent
|
id: animatedContent
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: false
|
clip: false
|
||||||
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
|
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||||
scale: modalContainer.scaleValue
|
scale: modalContainer.scaleValue
|
||||||
transformOrigin: Item.Center
|
transformOrigin: Item.Center
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
@@ -525,22 +505,13 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: root.shadowLevel
|
level: root.shadowLevel
|
||||||
fallbackOffset: root.shadowFallbackOffset
|
fallbackOffset: root.shadowFallbackOffset
|
||||||
targetRadius: root.effectiveCornerRadius
|
targetRadius: root.cornerRadius
|
||||||
targetColor: root.effectiveBackgroundColor
|
targetColor: root.backgroundColor
|
||||||
borderColor: root.effectiveBorderColor
|
borderColor: root.borderColor
|
||||||
borderWidth: root.effectiveBorderWidth
|
borderWidth: root.borderWidth
|
||||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: root.effectiveCornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor
|
|
||||||
border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth
|
|
||||||
z: 100
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: root.shouldBeVisible
|
focus: root.shouldBeVisible
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user