1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-12 23:32:50 -04:00

Compare commits

..

2 Commits

Author SHA1 Message Date
purian23 62bf9c6efe Add Directional Motion options 2026-03-04 10:14:00 -05:00
purian23 61a77bd186 Initial staging for Animation & Motion effects 2026-03-03 20:02:32 -05:00
262 changed files with 4876 additions and 55928 deletions
+1 -1
View File
@@ -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 }}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+8 -8
View File
@@ -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
+2 -2
View File
@@ -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 }}
+3 -6
View File
@@ -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
+4 -8
View File
@@ -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
+4 -4
View File
@@ -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
-8
View File
@@ -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
View File
@@ -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
+2 -6
View File
@@ -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
View File
@@ -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."
}
-76
View File
@@ -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)
}
-40
View File
@@ -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")
}
}
+1 -11
View File
@@ -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)
} }
+1 -13
View File
@@ -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)
+3 -3
View File
@@ -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,
} }
} }
+5 -5
View File
@@ -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()
} }
File diff suppressed because it is too large Load Diff
-87
View File
@@ -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")
}
}
+1 -5
View File
@@ -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,
}, },
} }
-8
View File
@@ -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 != "" {
+3 -4
View File
@@ -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)
-271
View File
@@ -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
View File
@@ -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())
+10 -3
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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
-35
View File
@@ -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
}
+16 -119
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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)
+18 -69
View File
@@ -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 {
+42 -52
View File
@@ -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
} }
-44
View File
@@ -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
}
+43 -164
View File
@@ -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 {
+29 -51
View File
@@ -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"),
}
File diff suppressed because it is too large Load Diff
-98
View File
@@ -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)
}
})
}
}
+2 -23
View File
@@ -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")
} }
-49
View File
@@ -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")
}
+1 -6
View File
@@ -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)
-892
View File
@@ -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()
}
-671
View File
@@ -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)
}
})
}
+13 -29
View File
@@ -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) {
+1 -5
View File
@@ -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)
-3
View File
@@ -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
+1 -5
View File
@@ -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"},
} }
+67 -99
View File
@@ -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
-4
View File
@@ -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: "",
+10 -50
View File
@@ -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)
} }
-1
View File
@@ -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)
+2 -9
View File
@@ -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)
}
} }
+27 -40
View File
@@ -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() {
+2 -8
View File
@@ -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 -27
View File
@@ -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 {
+5 -2
View File
@@ -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)
} }
+4 -17
View File
@@ -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
} }
} }
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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()
} }
+2 -2
View File
@@ -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]))
} }
} }
+1 -1
View File
@@ -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()
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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";
+3 -31
View File
@@ -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
+9 -17
View File
@@ -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))
+3 -5
View File
@@ -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 "=========================================="
+10 -78
View File
@@ -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"
+1 -1
View File
@@ -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.
-2
View File
@@ -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
+57 -85
View File
@@ -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
+2 -2
View File
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
# Code formatting and linting # Code formatting and linting
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat) qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .` qmllint **/*.qml # Lint all QML files
./qmlformat-all.sh # Format all QML files ./qmlformat-all.sh # Format all QML files
``` ```
@@ -783,7 +783,7 @@ When modifying the shell:
**QML Frontend:** **QML Frontend:**
1. **Test changes**: `qs -p .` (automatic reload on file changes) 1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml`, then from repo root run `make lint-qml` after Quickshell has generated the local `.qmlls.ini` VFS with `qs -p .` 2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml`
3. **Performance**: Ensure animations remain smooth (60 FPS target) 3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency 4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
+1 -6
View File
@@ -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")
+1 -10
View File
@@ -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
+4 -8
View File
@@ -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";
} }
+40 -51
View File
@@ -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;
} }
+40 -60
View File
@@ -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
+16 -228
View File
@@ -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();
} }
} }
} }
+9 -47
View File
@@ -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() {
+7 -8
View File
@@ -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;
+20 -645
View File
@@ -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();
} }
} }
+1 -6
View File
@@ -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() {
+4 -64
View File
@@ -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;
} }
+2
View File
@@ -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
View File
@@ -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
+8 -37
View File
@@ -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
-312
View File
@@ -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