1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -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
123 changed files with 4226 additions and 24934 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
+3 -3
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
+1 -1
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 }}
+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
+1 -5
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)..."
@@ -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)"
+1 -1
View File
@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.9.0 rev: v2.10.1
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
require_serial: true require_serial: true
@@ -1,11 +0,0 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter sync",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}
+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)
} }
File diff suppressed because it is too large Load Diff
-1
View File
@@ -19,7 +19,6 @@ 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", "greeter sync", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}
+10 -1
View File
@@ -16,10 +16,19 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
+10 -1
View File
@@ -11,20 +11,29 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
+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()
+8 -92
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,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType) args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil cmd.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
@@ -70,66 +44,16 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
if _, err := io.Copy(stdin, data); err != nil { if _, err := stdin.Write(data); err != nil {
stdin.Close() stdin.Close()
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("write stdin: %w", err)
} }
if err := stdin.Close(); err != nil { stdin.Close()
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error { func copyServe(data []byte, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -215,18 +139,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
if err := writeTo(file); err != nil { file.Write(data)
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil
+39 -18
View File
@@ -440,10 +440,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false hasNiri := false
hasQuickshell := false
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "niri-git" { if pkg == "niri-git" {
hasNiri = true hasNiri = true
} }
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
} }
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed // If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -597,16 +616,10 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
} }
srcinfoPath = filepath.Join(packageDir, ".SRCINFO") // Skip dependency installation for dms-shell-git and dms-shell-bin
if pkg == "dms-shell-bin" { // since we manually manage those dependencies
progressChan <- InstallProgressMsg{ if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
Phase: PhaseAURPackages, // Pre-install dependencies from .SRCINFO
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress), Progress: startProgress + 0.3*(endProgress-startProgress),
@@ -615,19 +628,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
CommandInfo: "Installing package dependencies and makedepends", CommandInfo: "Installing package dependencies and makedepends",
} }
// Install dependencies from .SRCINFO // Install dependencies and makedepends explicitly
depFilter := "" srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
if pkg == "dms-shell-git" {
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
}
depsCmd := exec.CommandContext(ctx, "bash", "-c", depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(` fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//') deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi fi
`, srcinfoPath, depFilter, sudoPassword)) `, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err) return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
@@ -644,6 +657,14 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err) return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
} }
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} }
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -656,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err) return fmt.Errorf("failed to build %s: %w", pkg, err)
+9 -59
View File
@@ -92,25 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
return debianPackageInstalledPrecisely(pkg) cmd := exec.Command("dpkg", "-l", pkg)
} err := cmd.Run()
return err == nil
func debianPackageInstalledPrecisely(pkg string) bool {
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
output, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(output)) == "installed"
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -210,12 +194,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: "Installing development dependencies...", Step: "Installing development dependencies...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools", LogOutput: "Installing additional development tools",
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -395,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
return names return names
} }
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool) enabledRepos := make(map[string]bool)
@@ -506,30 +482,12 @@ 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
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
startProgress := 0.40
endProgress := 0.60
if totalGroups > 1 {
if groupIndex == 1 {
endProgress = 0.50
} else {
startProgress = 0.50
}
}
args := d.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
Progress: startProgress, Progress: 0.40,
Step: "Installing system packages...", Step: "Installing system packages...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
@@ -537,15 +495,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress) return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+32 -42
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 {
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False") args = append(args, "--setopt=install_weak_deps=False")
} break
return append(args, packages...)
}
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
} }
} }
args := f.dnfInstallArgs(groupPackages, minimal) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: phase, Phase: PhaseAURPackages,
Progress: groupStart, Progress: 0.70,
Step: step, Step: "Installing COPR packages...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
}
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
}
+38 -158
View File
@@ -29,8 +29,6 @@ type OpenSUSEDistribution struct {
config DistroConfig config DistroConfig
} }
const openSUSENiriWaylandServerPackage = "libwayland-server0"
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan) base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{ return &OpenSUSEDistribution{
@@ -201,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
} }
func (o *OpenSUSEDistribution) getPrerequisites() []string { func (o *OpenSUSEDistribution) getPrerequisites() []string {
return []string{} return []string{
"make",
"unzip",
"gcc",
"gcc-c++",
"cmake",
"ninja",
"pkgconf-pkg-config",
"git",
"qt6-base-devel",
"qt6-declarative-devel",
"qt6-declarative-private-devel",
"qt6-shadertools",
"qt6-shadertools-devel",
"qt6-wayland-devel",
"qt6-waylandclient-private-devel",
"spirv-tools-devel",
"cli11-devel",
"wayland-protocols-devel",
"libgbm-devel",
"libdrm-devel",
"pipewire-devel",
"jemalloc-devel",
"wayland-utils",
"Mesa-libGLESv3-devel",
"pam-devel",
"glib2-devel",
"polkit-devel",
}
} }
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -271,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting prerequisite check...", LogOutput: "Starting prerequisite check...",
} }
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to disable install media repositories: %w", err)
}
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
@@ -305,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
NeedsSudo: true, NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
} }
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil { if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install zypper packages: %w", err) return fmt.Errorf("failed to install zypper packages: %w", err)
} }
} }
@@ -320,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
} }
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil { if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err) return fmt.Errorf("failed to install OBS packages: %w", err)
} }
} }
@@ -410,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
} }
} }
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
return systemPkgs, obsPkgs, manualPkgs, variantMap return systemPkgs, obsPkgs, manualPkgs, variantMap
} }
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
return nil
}
return []string{openSUSENiriWaylandServerPackage}
}
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
for _, pkg := range extraPkgs {
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
continue
}
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
systemPkgs = append(systemPkgs, pkg)
}
return systemPkgs
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string { func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages)) names := make([]string, len(packages))
for i, pkg := range packages { for i, pkg := range packages {
@@ -515,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
return nil return nil
} }
func isOpenSUSEInstallMediaURI(uri string) bool { func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
return strings.HasPrefix(normalizedURI, "cd:/") ||
strings.HasPrefix(normalizedURI, "dvd:/") ||
strings.HasPrefix(normalizedURI, "hd:/") ||
strings.HasPrefix(normalizedURI, "iso:/")
}
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 7 {
continue
}
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
alias := parts[1]
enabled := strings.ToLower(parts[3])
uri := parts[len(parts)-1]
if alias == "" || strings.EqualFold(alias, "alias") {
continue
}
if enabled != "" && enabled != "yes" {
continue
}
if !isOpenSUSEInstallMediaURI(uri) {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
output, err := listCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
return fmt.Errorf("failed to list zypper repositories: %w", err)
}
aliases := parseZypperInstallMediaAliases(string(output))
if len(aliases) == 0 {
return nil
}
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.055,
Step: "Disabling install media repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
}
for _, alias := range aliases {
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
}
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
}
return nil
}
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
args := []string{"zypper", "install", "-y"}
if minimal {
args = append(args, "--no-recommends")
}
return append(args, packages...)
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
} }
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages) args := []string{"zypper", "install", "-y"}
totalGroups := len(groups) args = append(args, packages...)
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := o.zypperInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: phase, Phase: PhaseSystemPackages,
Progress: groupStart, Progress: 0.40,
Step: step, Step: "Installing system packages...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+24 -46
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 {
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := u.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: phase, Phase: PhaseAURPackages,
Progress: groupStart, Progress: 0.70,
Step: step, Step: "Installing PPA packages...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -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
-1
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"}, ConfigFile: "zed.toml"},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
+1 -8
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)
} }
}
+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.
+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
+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.
+4 -10
View File
@@ -72,14 +72,9 @@
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ {
dms-shell = pkgs.lib.makeOverridable ( dms-shell = pkgs.buildGoModule (
{
extraQtPackages ? [ ],
}:
pkgs.buildGoModule (
let let
rootSrc = ./.; rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in in
{ {
inherit version; inherit version;
@@ -115,8 +110,8 @@
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs 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
@@ -146,8 +141,7 @@
platforms = pkgs.lib.platforms.linux; platforms = pkgs.lib.platforms.linux;
}; };
} }
) );
) { };
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;
+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
+174
View File
@@ -0,0 +1,174 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// AnimVariants Central tuning for animation and Motion Effects variants
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
Singleton {
id: root
readonly property list<real> variantEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standardDecel;
case 2:
return Anims.expressiveFastSpatial;
default:
return Anims.expressiveDefaultSpatial;
}
}
readonly property list<real> variantExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standard;
case 2:
return Anims.emphasized;
default:
return Anims.emphasized;
}
}
// Modal-specific entry curve
readonly property list<real> variantModalEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
}
return variantEnterCurve;
}
readonly property list<real> variantModalExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
// Popout-specific entry curve
readonly property list<real> variantPopoutEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
return Anims.standardDecel;
}
return variantEnterCurve;
}
readonly property list<real> variantPopoutExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
readonly property real variantEnterDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.9;
case 2:
return 1.08;
default:
return 1.0;
}
}
readonly property real variantExitDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.85;
case 2:
return 0.92;
default:
return 1.0;
}
}
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
readonly property real variantOpacityDurationScale: {
if (typeof SettingsData === "undefined")
return 1.0;
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
}
function variantDuration(baseDuration, entering) {
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
return Math.max(0, Math.round(baseDuration * factor));
}
function variantExitCleanupPadding() {
if (typeof SettingsData === "undefined")
return 50;
switch (SettingsData.motionEffect) {
case 1:
return 8;
case 2:
return 24;
default:
return 50;
}
}
function variantCloseInterval(baseDuration) {
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
}
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
readonly property real effectScaleCollapsed: {
if (typeof SettingsData === "undefined")
return 0.96;
switch (SettingsData.motionEffect) {
case 1:
return 1.0;
case 2:
return 0.88;
default:
return 0.96;
}
}
readonly property real effectAnimOffset: {
if (typeof SettingsData === "undefined")
return 16;
switch (SettingsData.motionEffect) {
case 1:
return 144;
case 2:
return 56;
default:
return 16;
}
}
}
+5
View File
@@ -22,4 +22,9 @@ Singleton {
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00] readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
// Used by AnimVariants for variant/effect logic
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
} }
+1 -1
View File
@@ -1245,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
+25 -83
View File
@@ -37,6 +37,18 @@ Singleton {
Custom Custom
} }
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 M3: scale-in, rises from below
Directional, // 1 pure large slide, no scale
Depth // 2 medium slide with deep depth scale pop
}
enum SuspendBehavior { enum SuspendBehavior {
Suspend, Suspend,
Hibernate, Hibernate,
@@ -166,6 +178,12 @@ Singleton {
property int modalCustomAnimationDuration: 150 property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings() onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings() onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12 property int m3ElevationIntensity: 12
@@ -280,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"
@@ -314,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"
@@ -468,20 +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 bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
property bool dockSmartAutoHide: false property bool dockSmartAutoHide: false
property bool dockGroupByApp: false property bool dockGroupByApp: false
property bool dockRestoreSpecialWorkspaceOnClick: false
property bool dockOpenOnOverview: false property bool dockOpenOnOverview: false
property int dockPosition: SettingsData.Position.Bottom property int dockPosition: SettingsData.Position.Bottom
property real dockSpacing: 4 property real dockSpacing: 4
@@ -527,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
@@ -566,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
@@ -1030,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();
} }
} }
@@ -1190,7 +1173,7 @@ Singleton {
"updateCompositorLayout": updateCompositorLayout, "updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs, "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor "updateCompositorCursor": updateCompositorCursor,
}) })
function set(key, value) { function set(key, value) {
@@ -1282,47 +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) {
@@ -2758,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) {
@@ -2767,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();
} }
} }
} }
+21 -5
View File
@@ -960,6 +960,24 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
} }
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
readonly property var animationPresetDurations: { readonly property var animationPresetDurations: {
"none": 0, "none": 0,
"short": 250, "short": 250,
@@ -1084,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";
} }
@@ -1551,7 +1569,7 @@ Singleton {
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");
@@ -1595,8 +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(","));
@@ -1989,7 +2005,7 @@ Singleton {
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json"; const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return colorsPath; return colorsPath;
} }
+21 -509
View File
@@ -10,352 +10,22 @@ Singleton {
property var settingsRoot: null property var settingsRoot: null
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string dankshellU2fPamText: ""
property string u2fKeysText: ""
property string fingerprintProbeOutput: ""
property int fingerprintProbeExitCode: 0
property bool fingerprintProbeStreamFinished: false
property bool fingerprintProbeExited: false
property string fingerprintProbeState: "probe_failed"
property string pamSupportProbeOutput: ""
property bool pamSupportProbeStreamFinished: false
property bool pamSupportProbeExited: false
property int pamSupportProbeExitCode: 0
property bool pamFprintSupportDetected: false
property bool pamU2fSupportDetected: false
readonly property string homeDir: Quickshell.env("HOME") || ""
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
function envFlag(name) {
const value = (Quickshell.env(name) || "").trim().toLowerCase();
if (value === "1" || value === "true" || value === "yes" || value === "on")
return true;
if (value === "0" || value === "false" || value === "no" || value === "off")
return false;
return null;
}
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
function detectQtTools() { function detectQtTools() {
qtToolsDetectionProcess.running = true; qtToolsDetectionProcess.running = true;
} }
function detectAuthCapabilities() {
if (!settingsRoot)
return;
if (forcedFprintAvailable === null) {
fingerprintProbeOutput = "";
fingerprintProbeStreamFinished = false;
fingerprintProbeExited = false;
fingerprintProbeProcess.running = true;
} else {
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
}
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
pamSupportProbeOutput = "";
pamSupportProbeStreamFinished = false;
pamSupportProbeExited = false;
pamSupportDetectionProcess.running = true;
}
recomputeAuthCapabilities();
}
function detectFprintd() { function detectFprintd() {
detectAuthCapabilities(); fprintdDetectionProcess.running = true;
} }
function detectU2f() { function detectU2f() {
detectAuthCapabilities(); u2fDetectionProcess.running = true;
} }
function checkPluginSettings() { function checkPluginSettings() {
pluginSettingsCheckProcess.running = true; pluginSettingsCheckProcess.running = true;
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function pamTextIncludesFile(pamText, filename) {
if (!pamText || !filename)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
return true;
}
return false;
}
function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName))
return true;
const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
return true;
}
return false;
}
function hasEnrolledFingerprintOutput(output) {
const lower = (output || "").toLowerCase();
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
return true;
const lines = lower.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed.startsWith("finger:"))
return true;
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
return true;
}
return false;
}
function hasMissingFingerprintEnrollmentOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no fingers enrolled")
|| lower.includes("no fingerprints enrolled")
|| lower.includes("no prints enrolled");
}
function hasMissingFingerprintReaderOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no devices available")
|| lower.includes("no device available")
|| lower.includes("no devices found")
|| lower.includes("list_devices failed")
|| lower.includes("no device");
}
function parseFingerprintProbe(exitCode, output) {
if (hasEnrolledFingerprintOutput(output))
return "ready";
if (hasMissingFingerprintEnrollmentOutput(output))
return "missing_enrollment";
if (hasMissingFingerprintReaderOutput(output))
return "missing_reader";
if (exitCode === 0)
return "missing_enrollment";
if (exitCode === 127 || (output || "").includes("__missing_command__"))
return "probe_failed";
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
}
function setLockFingerprintCapability(canEnable, ready, reason) {
settingsRoot.lockFingerprintCanEnable = canEnable;
settingsRoot.lockFingerprintReady = ready;
settingsRoot.lockFingerprintReason = reason;
}
function setLockU2fCapability(canEnable, ready, reason) {
settingsRoot.lockU2fCanEnable = canEnable;
settingsRoot.lockU2fReady = ready;
settingsRoot.lockU2fReason = reason;
}
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
settingsRoot.greeterFingerprintCanEnable = canEnable;
settingsRoot.greeterFingerprintReady = ready;
settingsRoot.greeterFingerprintReason = reason;
settingsRoot.greeterFingerprintSource = source;
}
function setGreeterU2fCapability(canEnable, ready, reason, source) {
settingsRoot.greeterU2fCanEnable = canEnable;
settingsRoot.greeterU2fReady = ready;
settingsRoot.greeterU2fReason = reason;
settingsRoot.greeterU2fSource = source;
}
function recomputeFingerprintCapabilities() {
if (forcedFprintAvailable !== null) {
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
const source = forcedFprintAvailable ? "dms" : "none";
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
return;
}
const state = fingerprintProbeState;
switch (state) {
case "ready":
setLockFingerprintCapability(true, true, "ready");
break;
case "missing_enrollment":
setLockFingerprintCapability(true, false, "missing_enrollment");
break;
case "missing_reader":
setLockFingerprintCapability(false, false, "missing_reader");
break;
case "missing_pam_support":
setLockFingerprintCapability(false, false, "missing_pam_support");
break;
default:
setLockFingerprintCapability(false, false, "probe_failed");
break;
}
if (greeterPamHasFprint) {
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
break;
default:
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
break;
}
return;
}
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "ready", "dms");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
break;
case "missing_pam_support":
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
break;
default:
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
break;
}
}
function recomputeU2fCapabilities() {
if (forcedU2fAvailable !== null) {
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
const source = forcedU2fAvailable ? "dms" : "none";
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
return;
}
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
const lockCanEnable = lockReady || pamU2fSupportDetected;
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
if (greeterPamHasU2f) {
setGreeterU2fCapability(true, true, "configured_externally", "pam");
return;
}
const greeterReady = homeU2fKeysDetected;
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
}
function recomputeAuthCapabilities() {
if (!settingsRoot)
return;
recomputeFingerprintCapabilities();
recomputeU2fCapabilities();
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
}
function finalizeFingerprintProbe() {
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
return;
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
function finalizePamSupportProbe() {
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
return;
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length !== 2)
continue;
if (parts[0] === "pam_fprintd.so")
pamFprintSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_u2f.so")
pamU2fSupportDetected = parts[1] === "true";
}
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
property var qtToolsDetectionProcess: Process { property var qtToolsDetectionProcess: Process {
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
running: false running: false
@@ -365,15 +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';
} }
} }
} }
@@ -381,181 +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; return;
root.finalizePamSupportProbe(); settingsRoot.u2fAvailable = (exitCode === 0);
}
}
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();
} }
} }
+4 -30
View File
@@ -47,6 +47,9 @@ var SPEC = {
modalAnimationSpeed: { def: 1 }, modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 }, modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
m3ElevationEnabled: { def: true }, m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 }, m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 }, m3ElevationOpacity: { def: 30 },
@@ -123,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" },
@@ -165,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 },
greeterEnableU2f: { def: false },
greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false },
greeterPadHours12Hour: { def: false },
greeterLockDateFormat: { def: "" },
greeterFontFamily: { def: "" },
greeterWallpaperFillMode: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },
@@ -284,19 +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 },
showDock: { def: false }, showDock: { def: false },
dockAutoHide: { def: false }, dockAutoHide: { def: false },
dockSmartAutoHide: { def: false }, dockSmartAutoHide: { def: false },
dockGroupByApp: { def: false }, dockGroupByApp: { def: false },
dockRestoreSpecialWorkspaceOnClick: { def: false },
dockOpenOnOverview: { def: false }, dockOpenOnOverview: { def: false },
dockPosition: { def: 1 }, dockPosition: { def: 1 },
dockSpacing: { def: 4 }, dockSpacing: { def: 4 },
@@ -341,23 +330,9 @@ var SPEC = {
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false },
lockFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintCanEnable: { def: false, persist: false },
greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false }, enableU2f: { def: false },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false }, u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false },
lockU2fReady: { def: false, persist: false },
lockU2fReason: { def: "probe_failed", persist: false },
greeterU2fCanEnable: { def: false, persist: false },
greeterU2fReady: { def: false, persist: false },
greeterU2fReason: { def: "probe_failed", persist: false },
greeterU2fSource: { def: "none", persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
@@ -380,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 },
+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 {
+1 -1
View File
@@ -313,7 +313,7 @@ Item {
} }
Variants { Variants {
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications") model: SettingsData.getFilteredScreens("notifications")
delegate: NotificationPopupManager { delegate: NotificationPopupManager {
modelData: item modelData: item
+165 -46
View File
@@ -26,10 +26,10 @@ Item {
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: 0.96 property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.spacingL property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.variantModalEnterCurve
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationExitCurve: Theme.variantModalExitCurve
property color backgroundColor: Theme.surfaceContainer property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
@@ -44,11 +44,13 @@ Item {
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool useOverlayLayer: false property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -58,19 +60,34 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen) {
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow) if (!useSingleWindow)
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
} }
ModalManager.openModal(root);
shouldBeVisible = true; if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow) if (!useSingleWindow)
clickCatcher.visible = true; clickCatcher.visible = true;
contentWindow.visible = true; contentWindow.visible = true;
}
ModalManager.openModal(root);
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false; shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible)); Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
} }
function close() { function close() {
@@ -131,7 +148,7 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration + 50 interval: Theme.variantCloseInterval(animationDuration)
onTriggered: { onTriggered: {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
@@ -145,7 +162,17 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
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: animationType === "slide" ? 30 : Math.max(0, animationOffset) readonly property real shadowMotionPadding: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -205,9 +232,26 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked() onClicked: root.backgroundClicked()
} }
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
} }
PanelWindow { PanelWindow {
@@ -250,9 +294,12 @@ Item {
bottom: root.useSingleWindow bottom: root.useSingleWindow
} }
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins { WlrLayershell.margins {
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) left: actualMarginLeft
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) top: actualMarginTop
right: 0 right: 0
bottom: 0 bottom: 0
} }
@@ -282,13 +329,14 @@ Item {
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
color: "black" color: "black"
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground visible: opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -296,8 +344,8 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: root.useSingleWindow ? root.alignedY : shadowBuffer y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
@@ -313,45 +361,117 @@ Item {
} }
readonly property bool slide: root.animationType === "slide" readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0 readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property real offsetY: slide ? -30 : root.animationOffset readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
property real animX: 0 readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
property real animY: 0 readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
property real scaleValue: root.animationScaleCollapsed readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) readonly property real customDistRight: root.screenWidth - customAnchorX
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
Connections { readonly property real offsetX: {
target: root if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
function onShouldBeVisibleChanged() { return 0;
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr); if (slide && !directionalEffect && !depthEffect)
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr); return 15;
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed; if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
} }
} }
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -367,15 +487,14 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: root.shouldBeVisible ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5 transformOrigin: Item.Center
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: animationDuration duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
@@ -1006,7 +1006,9 @@ Item {
_applyHighlights(newSections, searchQuery); _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections); flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
if (selectedFlatIndex >= flatModel.length) {
selectedFlatIndex = getFirstItemIndex(); selectedFlatIndex = getFirstItemIndex();
}
updateSelectedItem(); updateSelectedItem();
}); });
} }
@@ -14,6 +14,7 @@ Item {
property bool spotlightOpen: false property bool spotlightOpen: false
property bool keyboardActive: false property bool keyboardActive: false
property bool contentVisible: false property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
@@ -23,8 +24,14 @@ Item {
property string _pendingMode: "" property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
property real _frozenMotionY: 0
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen readonly property var effectiveScreen: contentWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -78,6 +85,34 @@ Item {
} }
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
// Shadow padding for the content window (render padding only, no motion padding)
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (Theme.isDirectionalEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) { function _ensureContentLoadedAndInitialize(query, mode) {
@@ -97,7 +132,8 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.searchField.forceActiveFocus(); // NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
@@ -130,40 +166,59 @@ Item {
} }
} }
function show() { function _openCommon(query, mode) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen(); // Disable animations so the snap is instant
if (focusedScreen) animationsEnabled = false;
launcherWindow.screen = focusedScreen;
spotlightOpen = true; // Freeze the collapsed offsets (they depend on height which could change)
keyboardActive = true; _frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
backgroundWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen;
}
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(root); ModalManager.openModal(root);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_ensureContentLoadedAndInitialize("", ""); // Load content and initialize (but no forceActiveFocus that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
Qt.callLater(() => {
root.animationsEnabled = true;
root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => {
root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField)
root.spotlightContent.searchField.forceActiveFocus();
});
});
}
function show() {
_openCommon("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
closeCleanupTimer.stop(); _openCommon(query, "");
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -171,13 +226,17 @@ Item {
return; return;
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect)
contentVisible = false; contentVisible = false;
// Trigger exit animation Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false; keyboardActive = false;
spotlightOpen = false; spotlightOpen = false;
focusGrab.active = false; focusGrab.active = false;
ModalManager.closeModal(root); ModalManager.closeModal(root);
closeCleanupTimer.start(); closeCleanupTimer.start();
} }
@@ -186,21 +245,7 @@ Item {
} }
function showWithMode(mode) { function showWithMode(mode) {
closeCleanupTimer.stop(); _openCommon("", mode);
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
} }
function toggleWithMode(mode) { function toggleWithMode(mode) {
@@ -221,10 +266,13 @@ Item {
Timer { Timer {
id: closeCleanupTimer id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50 interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
repeat: false repeat: false
onTriggered: { onTriggered: {
isClosing = false; isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose) if (root.unloadContentOnClose)
launcherContentLoader.active = false; launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
@@ -242,7 +290,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [launcherWindow] windows: [contentWindow]
active: false active: false
onCleared: { onCleared: {
@@ -267,7 +315,7 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screen = launcherWindow.screen; const screen = contentWindow.screen;
const screenName = screen?.name; const screenName = screen?.name;
let needsReset = !screen || !screenName; let needsReset = !screen || !screenName;
@@ -289,35 +337,31 @@ Item {
return; return;
root._windowEnabled = false; root._windowEnabled = false;
launcherWindow.screen = newScreen; backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => { Qt.callLater(() => {
root._windowEnabled = true; root._windowEnabled = true;
}); });
} }
} }
// Background window: fullscreen, handles darkening + click-to-dismiss
PanelWindow { PanelWindow {
id: launcherWindow id: backgroundWindow
visible: root._windowEnabled && (spotlightOpen || isClosing) visible: false
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: { WlrLayershell.layer: WlrLayershell.Top
switch (Quickshell.env("DMS_MODAL_LAYER")) { WlrLayershell.exclusiveZone: -1
case "bottom": WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; WlrLayershell.margins {
case "background": top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
return WlrLayershell.Top; left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
case "overlay": right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
} }
}
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
@@ -327,11 +371,11 @@ Item {
} }
mask: Region { mask: Region {
item: spotlightOpen ? fullScreenMask : null item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
} }
Item { Item {
id: fullScreenMask id: bgFullScreenMask
anchors.fill: parent anchors.fill: parent
} }
@@ -339,13 +383,14 @@ Item {
id: backgroundDarken id: backgroundDarken
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0 visible: launcherMotionVisible || opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim { DankAnim {
duration: Theme.modalAnimationDuration duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
} }
@@ -353,49 +398,179 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: spotlightOpen enabled: spotlightOpen
onClicked: mouse => { onClicked: root.hide()
var contentX = modalContainer.x;
var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
root.hide();
} }
} }
// Content window: SMALL, positioned with margins only renders the modal area
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
}
Item {
id: contentInputMask
visible: false
x: contentContainer.x + contentWrapper.x
y: contentContainer.y + contentWrapper.y
width: root.alignedWidth
height: root.alignedHeight
}
Item {
id: contentContainer
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: {
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// animX/animY are Behavior-animated DankPopout pattern
property real animX: 0
property real animY: 0
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
Component.onCompleted: {
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
Connections {
target: root
function on_MotionActiveChanged() {
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
}
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
} }
Item { Item {
id: modalContainer id: directionalClipMask
x: root.modalX readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
y: root.modalY readonly property real clipOversize: 2000
width: root.modalWidth
height: root.modalHeight
visible: contentVisible || opacity > 0
opacity: contentVisible ? 1 : 0 clip: shouldClip
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity { x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
DankAnim { y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scale { width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
DankAnim { height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Item {
id: aligner
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow { ElevationShadow {
id: launcherShadowLayer id: launcherShadowLayer
anchors.fill: parent width: parent.width
level: Theme.elevationLevel3 height: parent.height
fallbackOffset: 6 opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor targetColor: root.backgroundColor
borderColor: root.borderColor borderColor: root.borderColor
borderWidth: root.borderWidth borderWidth: root.borderWidth
@@ -403,6 +578,25 @@ Item {
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
// contentWrapper moves inside static contentContainer DankPopout pattern
Item {
id: contentWrapper
width: parent.width
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onPressed: mouse => mouse.accepted = true onPressed: mouse => mouse.accepted = true
@@ -435,6 +629,9 @@ Item {
event.accepted = true; event.accepted = true;
} }
} }
} } // contentWrapper
} } // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
} }
@@ -86,7 +86,7 @@ FocusScope {
Controller { Controller {
id: controller id: controller
active: root.parentModal?.spotlightOpen ?? true active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: root.viewModeContext viewModeContext: root.viewModeContext
onItemExecuted: { onItemExecuted: {
@@ -462,7 +462,7 @@ FocusScope {
showClearButton: true showClearButton: true
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? root.parentModal.spotlightOpen : true enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
placeholderText: "" placeholderText: ""
ignoreUpDownKeys: true ignoreUpDownKeys: true
ignoreTabKeys: true ignoreTabKeys: true
@@ -548,7 +548,6 @@ FocusScope {
} }
} }
} }
} }
Item { Item {
@@ -697,7 +696,13 @@ FocusScope {
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: root.parentModal?.isClosing ? 0 : 1 opacity: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList { ResultsList {
id: resultsList id: resultsList
@@ -468,7 +468,7 @@ Item {
switch (mode) { switch (mode) {
case "files": case "files":
if (!DSearchService.dsearchAvailable) if (!DSearchService.dsearchAvailable)
return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch"); return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
if (!hasQuery) if (!hasQuery)
return I18n.tr("Type to search files"); return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2) if (root.controller.searchQuery.length < 2)
+21 -105
View File
@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -12,45 +11,8 @@ FloatingWindow {
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
property bool awaitingFprintForPassword: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property string polkitEtcPamText: ""
property string polkitLibPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
readonly property bool polkitPamHasFprint: {
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
if (!polkitText)
return false;
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
}
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (line && line.includes(moduleName))
return true;
}
return false;
}
function focusPasswordField() { function focusPasswordField() {
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
} }
@@ -58,7 +20,6 @@ FloatingWindow {
function show() { function show() {
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
visible = true; visible = true;
Qt.callLater(focusPasswordField); Qt.callLater(focusPasswordField);
} }
@@ -67,27 +28,17 @@ FloatingWindow {
visible = false; visible = false;
} }
function _commitSubmit() {
isLoading = true;
awaitingFprintForPassword = false;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function submitAuth() { function submitAuth() {
if (!currentFlow || isLoading) if (!currentFlow || isLoading)
return; return;
if (!currentFlow.isResponseRequired) { isLoading = true;
awaitingFprintForPassword = true; currentFlow.submit(passwordInput);
return; passwordInput = "";
}
_commitSubmit();
} }
function cancelAuth() { function cancelAuth() {
if (isLoading) if (isLoading)
return; return;
awaitingFprintForPassword = false;
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest(); currentFlow.cancelAuthenticationRequest();
return; return;
@@ -109,7 +60,6 @@ FloatingWindow {
} }
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
} }
Connections { Connections {
@@ -133,11 +83,6 @@ FloatingWindow {
function onIsResponseRequiredChanged() { function onIsResponseRequiredChanged() {
if (!currentFlow.isResponseRequired) if (!currentFlow.isResponseRequired)
return; return;
if (awaitingFprintForPassword && passwordInput !== "") {
_commitSubmit();
return;
}
awaitingFprintForPassword = false;
isLoading = false; isLoading = false;
passwordInput = ""; passwordInput = "";
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
@@ -156,41 +101,6 @@ FloatingWindow {
} }
} }
FileView {
path: "/etc/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitEtcPamText = text()
onLoadFailed: root.polkitEtcPamText = ""
}
FileView {
path: "/usr/lib/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitLibPamText = text()
onLoadFailed: root.polkitLibPamText = ""
}
FileView {
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: root.systemAuthPamText = text()
onLoadFailed: root.systemAuthPamText = ""
}
FileView {
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: root.commonAuthPamText = text()
onLoadFailed: root.commonAuthPamText = ""
}
FileView {
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: root.passwordAuthPamText = text()
onLoadFailed: root.passwordAuthPamText = ""
}
FocusScope { FocusScope {
id: contentFocusScope id: contentFocusScope
@@ -295,31 +205,37 @@ FloatingWindow {
visible: text !== "" visible: text !== ""
} }
Rectangle {
width: parent.width
height: inputFieldHeight
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordField.activeFocus ? 2 : 1
opacity: isLoading ? 0.5 : 1
MouseArea {
anchors.fill: parent
enabled: !isLoading
onClicked: passwordField.forceActiveFocus()
}
DankTextField { DankTextField {
id: passwordField id: passwordField
width: parent.width anchors.fill: parent
height: inputFieldHeight
backgroundColor: Theme.surfaceHover
normalBorderColor: Theme.outlineStrong
focusedBorderColor: Theme.primary
borderWidth: 1
focusedBorderWidth: 2
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
leftIconSize: 20
leftIconColor: Theme.primary
leftIconFocusedColor: Theme.primary
opacity: isLoading ? 0.5 : 1
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText textColor: Theme.surfaceText
text: passwordInput text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false) showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: "" placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading enabled: !isLoading
onTextEdited: passwordInput = text onTextEdited: passwordInput = text
onAccepted: submitAuth() onAccepted: submitAuth()
} }
}
StyledText { StyledText {
text: I18n.tr("Authentication failed, please try again") text: I18n.tr("Authentication failed, please try again")
@@ -241,21 +241,6 @@ FocusScope {
} }
} }
Loader {
id: greeterLoader
anchors.fill: parent
active: root.currentIndex === 31
visible: active
focus: active
sourceComponent: GreeterTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader { Loader {
id: pluginsLoader id: pluginsLoader
anchors.fill: parent anchors.fill: parent
@@ -287,12 +287,6 @@ Rectangle {
"icon": "lock", "icon": "lock",
"tabIndex": 11 "tabIndex": 11
}, },
{
"id": "greeter",
"text": I18n.tr("Greeter"),
"icon": "login",
"tabIndex": 31
},
{ {
"id": "power_sleep", "id": "power_sleep",
"text": I18n.tr("Power & Sleep"), "text": I18n.tr("Power & Sleep"),
@@ -8,9 +8,6 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: "" property string _pendingMode: ""
property string _pendingQuery: "" property string _pendingQuery: ""
@@ -44,35 +41,8 @@ DankPopout {
openWithQuery(query); openWithQuery(query);
} }
readonly property int _baseWidth: { popupWidth: 560
switch (SettingsData.dankLauncherV2Size) { popupHeight: 640
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -136,7 +106,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: false property bool isClosing: appDrawerPopout.isClosing
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -12,7 +12,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:control-center" layerNamespace: "dms:control-center"
fullHeightSurface: true fullHeightSurface: false
property string expandedSection: "" property string expandedSection: ""
property var triggerScreen: null property var triggerScreen: null
@@ -126,9 +126,11 @@ DankPopout {
z: 5000 z: 5000
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: 200 duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
} }
@@ -1117,6 +1117,7 @@ Item {
if (!notificationCenterLoader.item) { if (!notificationCenterLoader.item) {
return; return;
} }
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) { if (notificationCenterLoader.item.setBarContext) {
@@ -390,11 +390,10 @@ BasePill {
anchors.top: parent.top anchors.top: parent.top
} }
NumericText { StyledText {
id: audioPercentV id: audioPercentV
visible: root.showAudioPercent visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -417,11 +416,10 @@ BasePill {
anchors.top: parent.top anchors.top: parent.top
} }
NumericText { StyledText {
id: micPercentV id: micPercentV
visible: root.showMicPercent visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -444,11 +442,10 @@ BasePill {
anchors.top: parent.top anchors.top: parent.top
} }
NumericText { StyledText {
id: brightnessPercentV id: brightnessPercentV
visible: root.showBrightnessPercent visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%" text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -539,8 +536,7 @@ BasePill {
} }
Rectangle { Rectangle {
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4 width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2 height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent" color: "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -556,23 +552,20 @@ BasePill {
anchors.leftMargin: 2 anchors.leftMargin: 2
} }
NumericText { StyledText {
id: audioPercent id: audioPercent
visible: root.showAudioPercent visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right anchors.left: audioIcon.right
anchors.leftMargin: 2 anchors.leftMargin: 2
width: reservedWidth
} }
} }
Rectangle { Rectangle {
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4 width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2 height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent" color: "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -588,22 +581,20 @@ BasePill {
anchors.leftMargin: 2 anchors.leftMargin: 2
} }
NumericText { StyledText {
id: micPercent id: micPercent
visible: root.showMicPercent visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: micIcon.right anchors.left: micIcon.right
anchors.leftMargin: 2 anchors.leftMargin: 2
width: reservedWidth
} }
} }
Rectangle { Rectangle {
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.reservedWidth : 0) + 4 width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.implicitWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2 height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent" color: "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -619,17 +610,15 @@ BasePill {
anchors.leftMargin: 2 anchors.leftMargin: 2
} }
NumericText { StyledText {
id: brightnessPercent id: brightnessPercent
visible: root.showBrightnessPercent visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%" text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: brightnessIcon.right anchors.left: brightnessIcon.right
anchors.leftMargin: 2 anchors.leftMargin: 2
width: reservedWidth
} }
} }
@@ -211,17 +211,16 @@ BasePill {
text: { text: {
const title = activeWindow && activeWindow.title ? activeWindow.title : ""; const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text; const appName = appText.text;
if (compactMode && title === appName) {
return title;
}
if (!title || !appName) { if (!title || !appName) {
return title; return title;
} }
if (title.endsWith(" - " + appName)) {
return title.substring(0, title.length - (" - " + appName).length);
}
if (title.endsWith(appName)) { if (title.endsWith(appName)) {
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, ""); return title.substring(0, title.length - appName.length).replace(/ - $/, "");
} }
return title; return title;
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
@@ -161,23 +162,6 @@ BasePill {
return 0; return 0;
} }
readonly property string autoBarShadowDirection: {
const edge = root.axis?.edge;
switch (edge) {
case "top":
return "top";
case "bottom":
return "bottom";
case "left":
return "left";
case "right":
return "right";
default:
return "bottom";
}
}
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
property bool menuOpen: false property bool menuOpen: false
property var currentTrayMenu: null property var currentTrayMenu: null
@@ -956,6 +940,13 @@ BasePill {
} }
})(), overflowMenu.dpr) })(), overflowMenu.dpr)
readonly property var elev: Theme.elevationLevel2
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: root.menuOpen ? 1 : 0 opacity: root.menuOpen ? 1 : 0
scale: root.menuOpen ? 1 : 0.85 scale: root.menuOpen ? 1 : 0.85
@@ -976,14 +967,19 @@ BasePill {
ElevationShadow { ElevationShadow {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
level: Theme.elevationLevel3 level: menuContainer.elev
direction: root.effectiveShadowDirection fallbackOffset: 4
fallbackOffset: 6 shadowBlurPx: menuContainer.shadowBlurPx
shadowSpreadPx: menuContainer.shadowSpreadPx
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true sourceRect.antialiasing: true
sourceRect.smooth: true sourceRect.smooth: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled shadowEnabled: Theme.elevationEnabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2)) layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1406,6 +1402,13 @@ BasePill {
} }
})(), menuWindow.dpr) })(), menuWindow.dpr)
readonly property var elev: Theme.elevationLevel2
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: menuRoot.showMenu ? 1 : 0 opacity: menuRoot.showMenu ? 1 : 0
scale: menuRoot.showMenu ? 1 : 0.85 scale: menuRoot.showMenu ? 1 : 0.85
@@ -1426,13 +1429,18 @@ BasePill {
ElevationShadow { ElevationShadow {
id: menuBgShadowLayer id: menuBgShadowLayer
anchors.fill: parent anchors.fill: parent
level: Theme.elevationLevel3 level: menuContainer.elev
direction: root.effectiveShadowDirection fallbackOffset: 4
fallbackOffset: 6 shadowBlurPx: menuContainer.shadowBlurPx
shadowSpreadPx: menuContainer.shadowSpreadPx
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true sourceRect.antialiasing: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled shadowEnabled: Theme.elevationEnabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1470,10 +1470,6 @@ Item {
delegate: Item { delegate: Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: rowAppIcon id: rowAppIcon
@@ -1489,14 +1485,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase() text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45 font.pixelSize: parent.width * 0.45
color: appGlyphColor color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -1507,14 +1503,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
size: parent.width * 0.7 size: parent.width * 0.7
name: "sports_esports" name: "sports_esports"
color: appGlyphColor color: Theme.primary
} }
} }
@@ -1527,12 +1523,11 @@ Item {
layer.effect: MultiEffect { layer.effect: MultiEffect {
saturation: 0 saturation: 0
colorization: 1 colorization: 1
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
} }
} }
IconImage { IconImage {
id: rowSteamIcon
anchors.fill: parent anchors.fill: parent
source: modelData.icon source: modelData.icon
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1543,21 +1538,11 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
size: root.appIconSize size: root.appIconSize
name: "sports_esports" name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon visible: modelData.isSteamApp && !modelData.icon
} }
Rectangle {
anchors.fill: parent
visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: focusedBorderColor
z: 1
}
MouseArea { MouseArea {
id: rowAppMouseArea id: rowAppMouseArea
anchors.fill: parent anchors.fill: parent
@@ -1639,10 +1624,6 @@ Item {
delegate: Item { delegate: Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: colAppIcon id: colAppIcon
@@ -1658,14 +1639,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase() text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45 font.pixelSize: parent.width * 0.45
color: appGlyphColor color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -1676,14 +1657,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
size: parent.width * 0.7 size: parent.width * 0.7
name: "sports_esports" name: "sports_esports"
color: appGlyphColor color: Theme.primary
} }
} }
@@ -1696,12 +1677,11 @@ Item {
layer.effect: MultiEffect { layer.effect: MultiEffect {
saturation: 0 saturation: 0
colorization: 1 colorization: 1
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
} }
} }
IconImage { IconImage {
id: colSteamIcon
anchors.fill: parent anchors.fill: parent
source: modelData.icon source: modelData.icon
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1712,21 +1692,11 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
size: root.appIconSize size: root.appIconSize
name: "sports_esports" name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon visible: modelData.isSteamApp && !modelData.icon
} }
Rectangle {
anchors.fill: parent
visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: focusedBorderColor
z: 1
}
MouseArea { MouseArea {
id: colAppMouseArea id: colAppMouseArea
anchors.fill: parent anchors.fill: parent
@@ -16,7 +16,6 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false
@@ -44,6 +44,43 @@ Item {
property int __volumeHoverCount: 0 property int __volumeHoverCount: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
function panelMotionX(panelWidth, active) {
if (active)
return 0;
if (directionalEffect) {
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
return isRightEdge ? -travel : travel;
}
if (depthEffect) {
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
return isRightEdge ? -travel : travel;
}
return 0;
}
function panelMotionY(panelType, panelHeight, active) {
if (active)
return 0;
if (directionalEffect) {
if (panelType === 2)
return panelHeight * 0.08;
if (panelType === 3)
return -panelHeight * 0.08;
return 0;
}
if (depthEffect) {
if (panelType === 2)
return panelHeight * 0.04;
if (panelType === 3)
return -panelHeight * 0.04;
return 0;
}
return 0;
}
function volumeAreaEntered() { function volumeAreaEntered() {
__volumeHoverCount++; __volumeHoverCount++;
panelEntered(); panelEntered();
@@ -62,30 +99,47 @@ Item {
visible: dropdownType === 1 && volumeAvailable visible: dropdownType === 1 && volumeAvailable
width: 60 width: 60
height: 180 height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
opacity: dropdownType === 1 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
scale: dropdownType === 1 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -197,33 +251,50 @@ Item {
Rectangle { Rectangle {
id: audioDevicesPanel id: audioDevicesPanel
visible: dropdownType === 2 visible: dropdownType === 2 && activePlayer !== null
width: 280 width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: dropdownType === 2 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
scale: dropdownType === 2 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -354,30 +425,47 @@ Item {
visible: dropdownType === 3 visible: dropdownType === 3
width: 240 width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: dropdownType === 3 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
scale: dropdownType === 3 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
+2 -43
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
@@ -134,40 +133,6 @@ Item {
function getGroupedToplevels() { function getGroupedToplevels() {
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []; return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
} }
function getHyprToplevelForWayland(waylandToplevel) {
if (!waylandToplevel || !CompositorService.isHyprland || !Hyprland.toplevels)
return null;
const hyprToplevels = Array.from(Hyprland.toplevels.values);
for (let i = 0; i < hyprToplevels.length; i++) {
if (hyprToplevels[i].wayland === waylandToplevel)
return hyprToplevels[i];
}
return null;
}
function getSpecialWorkspaceName(waylandToplevel) {
const hyprToplevel = getHyprToplevelForWayland(waylandToplevel);
if (!hyprToplevel)
return "";
const wsName = String(hyprToplevel.lastIpcObject?.workspace?.name || hyprToplevel.workspace?.name || "");
if (!wsName.startsWith("special:"))
return "";
return wsName.slice("special:".length);
}
function restoreSpecialWorkspaceWindow(waylandToplevel) {
if (!SettingsData.dockRestoreSpecialWorkspaceOnClick || !CompositorService.isHyprland || !waylandToplevel)
return false;
const specialName = getSpecialWorkspaceName(waylandToplevel);
if (!specialName)
return false;
Hyprland.dispatch("togglespecialworkspace " + specialName);
Qt.callLater(() => waylandToplevel.activate());
return true;
}
onIsHoveredChanged: { onIsHoveredChanged: {
if (mouseArea.pressed || dragging) if (mouseArea.pressed || dragging)
return; return;
@@ -311,11 +276,8 @@ Item {
break; break;
case "window": case "window":
const windowToplevel = getToplevelObject(); const windowToplevel = getToplevelObject();
if (windowToplevel) { if (windowToplevel)
if (restoreSpecialWorkspaceWindow(windowToplevel))
return;
windowToplevel.activate(); windowToplevel.activate();
}
break; break;
case "grouped": case "grouped":
if (appData.windowCount === 0) { if (appData.windowCount === 0) {
@@ -338,11 +300,8 @@ Item {
SessionService.launchDesktopEntry(groupedEntry); SessionService.launchDesktopEntry(groupedEntry);
} else if (appData.windowCount === 1) { } else if (appData.windowCount === 1) {
const groupedToplevel = getToplevelObject(); const groupedToplevel = getToplevelObject();
if (groupedToplevel) { if (groupedToplevel)
if (restoreSpecialWorkspaceWindow(groupedToplevel))
return;
groupedToplevel.activate(); groupedToplevel.activate();
}
} else if (contextMenu) { } else if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell"; const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
-20
View File
@@ -1,20 +0,0 @@
.pragma library
function readBoolOverride(envReader, names, fallbackValue) {
for (let i = 0; i < names.length; i++) {
const name = names[i];
const raw = envReader(name);
if (raw === undefined || raw === null || raw === "")
continue;
const normalized = String(raw).trim().toLowerCase();
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
return true;
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
return false;
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
}
return fallbackValue;
}
+8 -29
View File
@@ -4,16 +4,13 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter" readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
readonly property string sessionConfigPath: greetCfgDir + "/session.json" readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json" readonly property string memoryFile: greetCfgDir + "/memory.json"
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
property string lastSessionId: "" property string lastSessionId: ""
property string lastSuccessfulUser: "" property string lastSuccessfulUser: ""
@@ -52,44 +49,26 @@ Singleton {
if (!content || !content.trim()) if (!content || !content.trim())
return; return;
const memory = JSON.parse(content); const memory = JSON.parse(content);
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : ""; lastSessionId = memory.lastSessionId || "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : ""; lastSuccessfulUser = memory.lastSuccessfulUser || "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd memory:", e); console.warn("Failed to parse greetd memory:", e);
} }
} }
function saveMemory() { function saveMemory() {
let memory = {}; memoryFileView.setText(JSON.stringify({
if (rememberLastSession && lastSessionId) "lastSessionId": lastSessionId,
memory.lastSessionId = lastSessionId; "lastSuccessfulUser": lastSuccessfulUser
if (rememberLastUser && lastSuccessfulUser) }, null, 2));
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
} }
function setLastSessionId(id) { function setLastSessionId(id) {
if (!rememberLastSession) {
if (lastSessionId !== "") {
lastSessionId = "";
saveMemory();
}
return;
}
lastSessionId = id || ""; lastSessionId = id || "";
saveMemory(); saveMemory();
} }
function setLastSuccessfulUser(username) { function setLastSuccessfulUser(username) {
if (!rememberLastUser) {
if (lastSuccessfulUser !== "") {
lastSuccessfulUser = "";
saveMemory();
}
return;
}
lastSuccessfulUser = username || ""; lastSuccessfulUser = username || "";
saveMemory(); saveMemory();
} }
+10 -69
View File
@@ -5,22 +5,15 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property string configPath: { readonly property string configPath: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
return greetCfgDir + "/settings.json"; return greetCfgDir + "/settings.json";
} }
readonly property string _greeterCacheDir: {
const i = root.configPath.lastIndexOf("/");
return i >= 0 ? root.configPath.substring(0, i) : "";
}
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
property string currentThemeName: "purple" property string currentThemeName: "purple"
property bool settingsLoaded: false property bool settingsLoaded: false
property string customThemeFile: "" property string customThemeFile: ""
@@ -28,12 +21,6 @@ Singleton {
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property bool useFahrenheit: false property bool useFahrenheit: false
property bool nightModeEnabled: false property bool nightModeEnabled: false
property string weatherLocation: "New York, NY" property string weatherLocation: "New York, NY"
@@ -54,11 +41,6 @@ Singleton {
property string lockDateFormat: "" property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true property bool lockScreenShowPowerActions: true
property bool lockScreenShowProfileImage: true property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool powerActionConfirm: true property bool powerActionConfirm: true
property real powerActionHoldDuration: 0.5 property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -70,26 +52,14 @@ Singleton {
function parseSettings(content) { function parseSettings(content) {
try { try {
let settings = {};
if (content && content.trim()) { if (content && content.trim()) {
settings = JSON.parse(content); const settings = JSON.parse(content);
}
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple"; currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""; customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"; matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true; use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false; showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false; padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false; useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false; nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY"; weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
@@ -110,19 +80,6 @@ Singleton {
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : ""; lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true; lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true; lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
if (envRememberLastSession !== undefined) {
rememberLastSession = envRememberLastSession;
} else {
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
}
if (envRememberLastUser !== undefined) {
rememberLastUser = envRememberLastUser;
} else {
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
}
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true; powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5; powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]; powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
@@ -131,6 +88,7 @@ Singleton {
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({}); screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2; animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill"; wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
settingsLoaded = true;
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) { if (currentThemeName === "custom" && customThemeFile) {
@@ -138,35 +96,22 @@ Singleton {
} }
Theme.applyGreeterTheme(currentThemeName); Theme.applyGreeterTheme(currentThemeName);
} }
}
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd settings:", e); console.warn("Failed to parse greetd settings:", e);
} finally {
settingsLoaded = true;
} }
} }
function getEffectiveTimeFormat() { function getEffectiveTimeFormat() {
const use24 = greeterUse24HourClock; if (use24HourClock)
const secs = greeterShowSeconds; return showSeconds ? "hh:mm:ss" : "hh:mm";
const pad = greeterPadHours12Hour; if (padHours12Hour)
if (use24) return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "hh:mm:ss" : "hh:mm"; return showSeconds ? "h:mm:ss AP" : "h:mm AP";
if (pad)
return secs ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "h:mm:ss AP" : "h:mm AP";
} }
function getEffectiveLockDateFormat() { function getEffectiveLockDateFormat() {
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat; return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
}
function getEffectiveWallpaperFillMode() {
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
}
function getEffectiveFontFamily() {
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
} }
function getFilteredScreens(componentId) { function getFilteredScreens(componentId) {
@@ -188,9 +133,5 @@ Singleton {
onLoaded: { onLoaded: {
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
onLoadFailed: error => {
console.warn("Failed to load greetd settings:", error);
root.parseSettings("");
}
} }
} }
+45 -675
View File
@@ -31,35 +31,6 @@ Item {
signal launchRequested signal launchRequested
property bool weatherInitialized: false property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property bool pendingPasswordResponse: false
property bool passwordSubmitRequested: false
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 10000
property int externalAuthTimeoutMs: 30000
property int memoryFlushDelayMs: 120
property string pendingLaunchCommand: ""
property var pendingLaunchEnv: []
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string faillockConfigText: ""
property bool greeterWallpaperOverrideExists: false
property string externalAuthAutoStartedForUser: ""
property int passwordSessionTransitionRetryCount: 0
property int maxPasswordSessionTransitionRetries: 2
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
function initWeatherService() { function initWeatherService() {
if (weatherInitialized) if (weatherInitialized)
@@ -73,357 +44,16 @@ Item {
WeatherService.forceRefresh(); WeatherService.forceRefresh();
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function pamTextIncludesFile(pamText, filename) {
if (!pamText || !filename)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
return true;
}
return false;
}
function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName))
return true;
const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
return true;
}
return false;
}
function usesPamLockoutPolicy(pamText) {
if (!pamText)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
return true;
}
return false;
}
function parsePamLineDenyValue(pamText) {
if (!pamText)
return -1;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
continue;
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function parseFaillockDenyValue(configText) {
if (!configText)
return -1;
const lines = configText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function refreshPasswordAttemptPolicyHint() {
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
let lockoutConfigured = false;
let denyFromPam = -1;
for (let i = 0; i < pamSources.length; i++) {
const source = pamSources[i];
if (!source)
continue;
if (usesPamLockoutPolicy(source))
lockoutConfigured = true;
const denyValue = parsePamLineDenyValue(source);
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
denyFromPam = denyValue;
}
if (!lockoutConfigured) {
passwordAttemptLimitHint = 0;
return;
}
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
if (denyFromConfig >= 0) {
passwordAttemptLimitHint = denyFromConfig;
return;
}
if (denyFromPam >= 0) {
passwordAttemptLimitHint = denyFromPam;
return;
}
// pam_faillock default deny value when no explicit config is set.
passwordAttemptLimitHint = 3;
}
function isLikelyLockoutMessage(message) {
const lower = (message || "").toLowerCase();
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
}
function currentAuthMessage() {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "max")
return "Too many failed attempts - account may be locked";
if (GreeterState.pamState === "fail") {
if (passwordAttemptLimitHint > 0) {
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
if (remaining > 0) {
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
}
return "Incorrect password - next failures may trigger account lockout";
}
return "Incorrect password";
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
}
function resetPasswordSessionTransition(clearSubmitRequest) {
cancelingExternalAuthForPassword = false;
passwordSessionTransitionRetryCount = 0;
if (clearSubmitRequest)
passwordSubmitRequested = false;
}
Connections { Connections {
target: GreetdSettings target: GreetdSettings
function onSettingsLoadedChanged() { function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) { if (GreetdSettings.settingsLoaded)
initWeatherService(); initWeatherService();
if (isPrimaryScreen) {
applyLastSuccessfulUser();
finalizeSessionSelection();
}
}
}
function onRememberLastUserChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
applyLastSuccessfulUser();
}
function onRememberLastSessionChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
finalizeSessionSelection();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.loginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: faillockConfigWatcher
path: "/etc/security/faillock.conf"
printErrors: false
onLoaded: {
root.faillockConfigText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.faillockConfigText = "";
root.refreshPasswordAttemptPolicyHint();
} }
} }
Component.onCompleted: { Component.onCompleted: {
initWeatherService(); initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen) if (isPrimaryScreen)
applyLastSuccessfulUser(); applyLastSuccessfulUser();
@@ -433,131 +63,15 @@ Item {
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser; const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser; GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser; GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true; GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser); PortalService.getGreeterUserProfileImage(lastUser);
maybeAutoStartExternalAuth();
} }
} }
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
maybeAutoStartExternalAuth();
}
function submitBufferedPassword() {
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
Greetd.respond(GreeterState.passwordBuffer || "");
GreeterState.passwordBuffer = "";
inputField.text = "";
return true;
}
function requestPasswordSessionTransition() {
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && !hasPasswordBuffer)
return;
if (cancelingExternalAuthForPassword)
return;
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
pendingPasswordResponse = false;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
resetPasswordSessionTransition(true);
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
return;
}
cancelingExternalAuthForPassword = true;
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
Greetd.cancelSession();
}
function startAuthSession(submitPassword) {
submitPassword = submitPassword === true;
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking)
return;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (Greetd.state !== GreetdState.Inactive) {
if (pendingPasswordResponse && submitPassword)
submitBufferedPassword();
else if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (cancelingExternalAuthForPassword) {
if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
return;
pendingPasswordResponse = false;
passwordSubmitRequested = submitPassword;
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
// Included PAM stacks (system-auth/common-auth/password-auth) may still run
// biometric/U2F modules before password even when DMS toggles are off.
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterPamHasExternalAuth;
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function maybeAutoStartExternalAuth() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (!root.greeterExternalAuthAvailable)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
return;
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
return;
if (externalAuthAutoStartedForUser === GreeterState.username)
return;
externalAuthAutoStartedForUser = GreeterState.username;
startAuthSession(false);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized) if (weatherInitialized)
WeatherService.removeRef(); WeatherService.removeRef();
@@ -629,39 +143,10 @@ Item {
} }
} }
FileView {
id: greeterWallpaperOverrideFile
path: GreetdSettings.greeterWallpaperOverridePath
printErrors: false
watchChanges: true
onLoaded: root.greeterWallpaperOverrideExists = true
onLoadFailed: root.greeterWallpaperOverrideExists = false
}
Connections {
target: GreetdSettings
function onGreeterWallpaperOverridePathChanged() {
if (!GreetdSettings.greeterWallpaperOverridePath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
function onGreeterWallpaperPathChanged() {
if (!GreetdSettings.greeterWallpaperPath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
}
DankBackdrop { DankBackdrop {
anchors.fill: parent anchors.fill: parent
screenName: root.screenName screenName: root.screenName
visible: { visible: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return false;
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
@@ -674,14 +159,12 @@ Item {
anchors.fill: parent anchors.fill: parent
source: { source: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : ""; return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
} }
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode()) fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
smooth: true smooth: true
asynchronous: false asynchronous: false
cache: true cache: true
@@ -844,7 +327,10 @@ Item {
anchors.top: clockContainer.bottom anchors.top: clockContainer.bottom
anchors.topMargin: 4 anchors.topMargin: 4
text: { text: {
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat()); if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
}
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
} }
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: "white" color: "white"
@@ -913,9 +399,6 @@ Item {
if (GreeterState.showPasswordInput && revealButton.visible) { if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width; margin += revealButton.width;
} }
if (externalAuthButton.visible) {
margin += externalAuthButton.width;
}
if (virtualKeyboardButton.visible) { if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width; margin += virtualKeyboardButton.width;
} }
@@ -932,18 +415,21 @@ Item {
return; return;
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text; GreeterState.passwordBuffer = text;
if (!text || text.length === 0)
root.passwordSubmitRequested = false;
} else { } else {
GreeterState.usernameInput = text; GreeterState.usernameInput = text;
} }
} }
onAccepted: { onAccepted: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
root.startAuthSession(true); if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (text.trim()) { if (text.trim()) {
root.submitUsername(text); GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true; syncingFromState = true;
text = ""; text = "";
syncingFromState = false; syncingFromState = false;
@@ -975,14 +461,14 @@ Item {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (GreeterState.unlocking) { if (GreeterState.unlocking) {
return "Logging in..."; return "Logging in...";
} }
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) { if (Greetd.state !== GreetdState.Inactive) {
return "Authenticating..."; return "Authenticating...";
} }
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
@@ -990,7 +476,7 @@ Item {
} }
return "Username..."; return "Username...";
} }
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0 opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
@@ -1012,7 +498,7 @@ Item {
StyledText { StyledText {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -1042,27 +528,15 @@ Item {
DankActionButton { DankActionButton {
id: revealButton id: revealButton
anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)) anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0 anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility" iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32 buttonSize: 32
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: parent.showPassword = !parent.showPassword onClicked: parent.showPassword = !parent.showPassword
} }
DankActionButton {
id: externalAuthButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
buttonSize: 32
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible
onClicked: root.startAuthSession(false)
}
DankActionButton { DankActionButton {
id: virtualKeyboardButton id: virtualKeyboardButton
@@ -1071,7 +545,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboard_controller.isKeyboardActive) { if (keyboard_controller.isKeyboardActive) {
@@ -1090,14 +564,19 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return" iconName: "keyboard_return"
buttonSize: 36 buttonSize: 36
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: true enabled: true
onClicked: { onClicked: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
root.startAuthSession(true); if (GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (inputField.text.trim()) { if (inputField.text.trim()) {
root.submitUsername(inputField.text); GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
} }
} }
@@ -1122,16 +601,20 @@ Item {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 38 Layout.preferredHeight: 20
Layout.topMargin: -Theme.spacingS Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS Layout.bottomMargin: -Theme.spacingS
text: root.authFeedbackMessage text: {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password";
return "";
}
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap opacity: GreeterState.pamState !== "" ? 1 : 0
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -1184,7 +667,6 @@ Item {
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: { onClicked: {
GreeterState.reset(); GreeterState.reset();
root.externalAuthAutoStartedForUser = "";
inputField.text = ""; inputField.text = "";
PortalService.profileImage = ""; PortalService.profileImage = "";
} }
@@ -1547,11 +1029,9 @@ Item {
return; return;
if (!GreetdMemory.memoryReady) if (!GreetdMemory.memoryReady)
return; return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : ""; const savedSession = GreetdMemory.lastSessionId;
if (savedSession && GreetdSettings.rememberLastSession) { if (savedSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) { if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i; GreeterState.currentSessionIndex = i;
@@ -1684,151 +1164,44 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) { function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) { if (responseRequired) {
cancelingExternalAuthForPassword = false; Greetd.respond(GreeterState.passwordBuffer);
passwordSessionTransitionRetryCount = 0; GreeterState.passwordBuffer = "";
awaitingExternalAuth = false; inputField.text = "";
pendingPasswordResponse = true;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && hasPasswordBuffer)
passwordSubmitRequested = true;
if (passwordSubmitRequested && !root.submitBufferedPassword())
passwordSubmitRequested = false;
if (passwordSubmitRequested || hasPasswordBuffer) {
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
} else {
authTimeout.stop();
}
return; return;
} }
pendingPasswordResponse = false; if (!error)
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
if (!passwordSubmitRequested)
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
authTimeout.interval = externalAuthTimeoutMs;
else
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond(""); Greetd.respond("");
} }
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
if (resumePasswordSubmit) {
Qt.callLater(function() {
root.startAuthSession(true);
});
return;
}
resetPasswordSessionTransition(true);
}
}
function onReadyToLaunch() { function onReadyToLaunch() {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
passwordFailureCount = 0;
clearAuthFeedback();
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]; const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex]; const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
if (!sessionCmd) { if (!sessionCmd) {
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
return; return;
} }
GreeterState.unlocking = true; GreeterState.unlocking = true;
launchTimeout.restart(); launchTimeout.restart();
if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSessionId(sessionPath); GreetdMemory.setLastSessionId(sessionPath);
} else if (GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username); GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) { Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
GreetdMemory.setLastSuccessfulUser("");
}
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
memoryFlushTimer.restart();
} }
function onAuthFailure(message) { function onAuthFailure(message) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
if (isLikelyLockoutMessage(message)) {
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail"; GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession();
} }
function onError(error) { function onError(error) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
}
Timer {
id: memoryFlushTimer
interval: memoryFlushDelayMs
onTriggered: {
if (!pendingLaunchCommand)
return;
const sessionCommand = pendingLaunchCommand;
const launchEnv = pendingLaunchEnv;
pendingLaunchCommand = "";
pendingLaunchEnv = [];
Greetd.launch(sessionCommand.split(" "), launchEnv);
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
@@ -1842,11 +1215,8 @@ Item {
onTriggered: { onTriggered: {
if (!GreeterState.unlocking) if (!GreeterState.unlocking)
return; return;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession(); Greetd.cancelSession();
} }
@@ -1855,7 +1225,7 @@ Item {
Timer { Timer {
id: placeholderDelay id: placeholderDelay
interval: 4000 interval: 4000
onTriggered: clearAuthFeedback() onTriggered: GreeterState.pamState = ""
} }
LockPowerMenu { LockPowerMenu {
-2
View File
@@ -9,7 +9,6 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc. - **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd` - **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
- **Session Memory**: Remembers last selected session and user - **Session Memory**: Remembers last selected session and user
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
## Installation ## Installation
@@ -213,7 +212,6 @@ dms-greeter --command hyprland
dms-greeter --command sway dms-greeter --command sway
dms-greeter --command mangowc dms-greeter --command mangowc
dms-greeter --command niri -C /path/to/custom-niri.kdl dms-greeter --command niri -C /path/to/custom-niri.kdl
dms-greeter --command niri --remember-last-user false --remember-last-session false
``` ```
Configure greetd to use it in `/etc/greetd/config.toml`: Configure greetd to use it in `/etc/greetd/config.toml`:
+11 -146
View File
@@ -6,9 +6,6 @@ COMPOSITOR=""
COMPOSITOR_CONFIG="" COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter" DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter" CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() { show_help() {
cat << EOF cat << EOF
@@ -25,15 +22,6 @@ Options:
(default: dms-greeter) (default: dms-greeter)
--cache-dir PATH Cache directory for greeter data --cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter) (default: /var/cache/dms-greeter)
--remember-last-session BOOL
Persist selected session to greeter memory
(BOOL: true/false, default: from settings.json)
--remember-last-user BOOL
Persist last successful username to greeter memory
(BOOL: true/false, default: from settings.json)
--no-save-session Alias for --remember-last-session false
--no-save-username Alias for --remember-last-user false
--debug Enable verbose startup logging to stderr
-h, --help Show this help message -h, --help Show this help message
Examples: Examples:
@@ -42,7 +30,6 @@ Examples:
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango dms-greeter --command mango
dms-greeter --command labwc dms-greeter --command labwc
EOF EOF
@@ -56,41 +43,6 @@ require_command() {
fi fi
} }
normalize_bool_flag() {
local flag_name="$1"
local value="$2"
local normalized="${value,,}"
case "$normalized" in
1|true|yes|on)
echo "1"
;;
0|false|no|off)
echo "0"
;;
*)
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
exit 1
;;
esac
}
exec_compositor() {
local log_tag="$1"
shift
if [[ "$DEBUG_MODE" == "1" ]]; then
exec "$@"
fi
if command -v systemd-cat >/dev/null 2>&1; then
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
fi
local log_file="$CACHE_DIR/$log_tag.log"
exec "$@" >> "$log_file" 2>&1
}
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--command) --command)
@@ -109,26 +61,6 @@ while [[ $# -gt 0 ]]; do
CACHE_DIR="$2" CACHE_DIR="$2"
shift 2 shift 2
;; ;;
--remember-last-session)
REMEMBER_LAST_SESSION="$2"
shift 2
;;
--remember-last-user)
REMEMBER_LAST_USER="$2"
shift 2
;;
--no-save-session)
REMEMBER_LAST_SESSION="0"
shift
;;
--no-save-username)
REMEMBER_LAST_USER="0"
shift
;;
--debug)
DEBUG_MODE=1
shift
;;
-h|--help) -h|--help)
show_help show_help
exit 0 exit 0
@@ -179,58 +111,9 @@ export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1 export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1 export DMS_RUN_GREETER=1
ensure_cache_tree() {
local base="$1"
mkdir -p "$base/.local/state" "$base/.local/state/wireplumber" "$base/.local/share" "$base/.cache"
}
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
CACHE_DIR="$FALLBACK_CACHE_DIR"
if ! ensure_cache_tree "$CACHE_DIR"; then
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
exit 1
fi
fi
export DMS_GREET_CFG_DIR="$CACHE_DIR" export DMS_GREET_CFG_DIR="$CACHE_DIR"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then mkdir -p "$CACHE_DIR"
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
if command -v qs >/dev/null 2>&1; then if command -v qs >/dev/null 2>&1; then
QS_BIN="qs" QS_BIN="qs"
@@ -247,9 +130,7 @@ if [[ "$DMS_PATH" == /* ]]; then
else else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH") RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
if [[ "$DEBUG_MODE" == "1" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2 echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
QS_CMD="$QS_BIN -p $RESOLVED_PATH" QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2 echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
@@ -270,10 +151,6 @@ hotkey-overlay {
environment { environment {
DMS_RUN_GREETER "1" DMS_RUN_GREETER "1"
HOME "$CACHE_DIR"
XDG_STATE_HOME "$CACHE_DIR/.local/state"
XDG_DATA_HOME "$CACHE_DIR/.local/share"
XDG_CACHE_HOME "$CACHE_DIR/.cache"
} }
debug { debug {
@@ -315,7 +192,7 @@ NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation" spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG" exec niri -c "$COMPOSITOR_CONFIG"
;; ;;
hyprland) hyprland)
@@ -327,10 +204,6 @@ NIRI_EOF
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1 env = DMS_RUN_GREETER,1
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
misc { misc {
disable_hyprland_logo = true disable_hyprland_logo = true
@@ -344,18 +217,14 @@ HYPRLAND_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
if command -v start-hyprland >/dev/null 2>&1; then if command -v start-hyprland >/dev/null 2>&1; then
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG" exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
else else
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG" exec Hyprland -c "$COMPOSITOR_CONFIG"
fi fi
;; ;;
@@ -364,7 +233,6 @@ HYPRLAND_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SWAY_EOF cat > "$TEMP_CONFIG" << SWAY_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; swaymsg exit" exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -373,12 +241,11 @@ SWAY_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SWAY_EOF cat >> "$TEMP_CONFIG" << SWAY_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; swaymsg exit" exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG" exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;; ;;
scroll) scroll)
@@ -386,7 +253,6 @@ SWAY_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SCROLL_EOF cat > "$TEMP_CONFIG" << SCROLL_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; scrollmsg exit" exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -395,12 +261,11 @@ SCROLL_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SCROLL_EOF cat >> "$TEMP_CONFIG" << SCROLL_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; scrollmsg exit" exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG" exec scroll -c "$COMPOSITOR_CONFIG"
;; ;;
miracle|miracle-wm) miracle|miracle-wm)
@@ -420,24 +285,24 @@ exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG" exec miracle-wm -c "$COMPOSITOR_CONFIG"
;; ;;
labwc) labwc)
require_command "labwc" require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD" exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else else
exec_compositor "labwc" labwc --session "$QS_CMD" exec labwc --session "$QS_CMD"
fi fi
;; ;;
mango|mangowc) mango|mangowc)
require_command "mango" require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
else else
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit" exec mango -s "$QS_CMD && mmsg -d quit"
fi fi
;; ;;
+2 -17
View File
@@ -755,7 +755,7 @@ Item {
} }
} }
onAccepted: { onAccepted: {
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) { if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
pam.passwd.start(); pam.passwd.start();
} }
} }
@@ -764,11 +764,6 @@ Item {
return; return;
} }
if (root.unlocking) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (pam.u2fPending) { if (pam.u2fPending) {
pam.cancelU2fPending(); pam.cancelU2fPending();
@@ -1022,7 +1017,7 @@ Item {
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending)) visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
enabled: !demoMode enabled: !demoMode
onClicked: { onClicked: {
if (!demoMode && !root.unlocking && !pam.u2fPending) { if (!demoMode && !pam.u2fPending) {
pam.passwd.start(); pam.passwd.start();
} }
} }
@@ -1631,7 +1626,6 @@ Item {
onStateChanged: { onStateChanged: {
root.pamState = state; root.pamState = state;
if (state !== "") { if (state !== "") {
root.unlocking = false;
placeholderDelay.restart(); placeholderDelay.restart();
passwordField.text = ""; passwordField.text = "";
root.passwordBuffer = ""; root.passwordBuffer = "";
@@ -1647,15 +1641,6 @@ Item {
} }
} }
Connections {
target: pam
function onUnlockInProgressChanged() {
if (!pam.unlockInProgress && root.unlocking)
root.unlocking = false;
}
}
Binding { Binding {
target: pam target: pam
property: "buffer" property: "buffer"
+36 -88
View File
@@ -25,29 +25,6 @@ Scope {
signal flashMsg signal flashMsg
signal unlockRequested signal unlockRequested
function resetAuthFlows(): void {
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockInProgress = false;
}
function recoverFromAuthStall(newState: string): void {
resetAuthFlows();
state = newState;
flashMsg();
stateReset.restart();
fprint.checkAvail();
u2f.checkAvail();
}
function completeUnlock(): void { function completeUnlock(): void {
if (!unlockInProgress) { if (!unlockInProgress) {
unlockInProgress = true; unlockInProgress = true;
@@ -59,7 +36,6 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; u2fPending = false;
u2fState = ""; u2fState = "";
unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
} }
} }
@@ -90,13 +66,6 @@ Scope {
printErrors: false printErrors: false
} }
FileView {
id: loginConfigWatcher
path: "/etc/pam.d/login"
printErrors: false
}
FileView { FileView {
id: u2fConfigWatcher id: u2fConfigWatcher
@@ -108,7 +77,7 @@ Scope {
id: passwd id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login" config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message.startsWith("The account is locked")) if (message.startsWith("The account is locked"))
@@ -133,13 +102,6 @@ Scope {
return; return;
} }
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error) if (res === PamResult.Error)
root.state = "error"; root.state = "error";
else if (res === PamResult.MaxTries) else if (res === PamResult.MaxTries)
@@ -152,22 +114,10 @@ Scope {
} }
} }
Connections {
target: passwd
function onActiveChanged() {
if (passwd.active) {
passwdActiveTimeout.restart();
} else {
passwdActiveTimeout.running = false;
}
}
}
PamContext { PamContext {
id: fprint id: fprint
property bool available: SettingsData.lockFingerprintReady property bool available
property int tries property int tries
property int errorTries property int errorTries
@@ -223,7 +173,7 @@ Scope {
PamContext { PamContext {
id: u2f id: u2f
property bool available: SettingsData.lockU2fReady property bool available
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) { if (!available || !SettingsData.enableU2f || !root.lockSecured) {
@@ -252,7 +202,7 @@ Scope {
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message !== "") if (message.toLowerCase().includes("touch"))
root.u2fState = "waiting"; root.u2fState = "waiting";
} }
@@ -288,6 +238,26 @@ Scope {
} }
} }
Process {
id: availProc
command: ["sh", "-c", "fprintd-list $USER"]
onExited: code => {
fprint.available = code === 0;
fprint.checkAvail();
}
}
Process {
id: u2fAvailProc
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
onExited: code => {
u2f.available = code === 0;
u2f.checkAvail();
}
}
Timer { Timer {
id: errorRetry id: errorRetry
@@ -309,26 +279,6 @@ Scope {
onTriggered: root.cancelU2fPending() onTriggered: root.cancelU2fPending()
} }
Timer {
id: passwdActiveTimeout
interval: 15000
onTriggered: {
if (passwd.active)
root.recoverFromAuthStall("error");
}
}
Timer {
id: unlockRequestTimeout
interval: 8000
onTriggered: {
if (root.unlockInProgress)
root.recoverFromAuthStall("error");
}
}
Timer { Timer {
id: stateReset id: stateReset
@@ -351,17 +301,24 @@ Scope {
onLockSecuredChanged: { onLockSecuredChanged: {
if (lockSecured) { if (lockSecured) {
SettingsData.refreshAuthAvailability(); availProc.running = true;
u2fAvailProc.running = true;
root.state = ""; root.state = "";
root.fprintState = ""; root.fprintState = "";
root.u2fState = ""; root.u2fState = "";
root.u2fPending = false; root.u2fPending = false;
root.lockMessage = ""; root.lockMessage = "";
root.resetAuthFlows(); root.unlockInProgress = false;
fprint.checkAvail();
u2f.checkAvail();
} else { } else {
root.resetAuthFlows(); fprint.abort();
passwd.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
root.unlockInProgress = false;
} }
} }
@@ -372,24 +329,15 @@ Scope {
fprint.checkAvail(); fprint.checkAvail();
} }
function onLockFingerprintReadyChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void { function onEnableU2fChanged(): void {
u2f.checkAvail(); u2f.checkAvail();
} }
function onLockU2fReadyChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void { function onU2fModeChanged(): void {
if (root.lockSecured) { if (root.lockSecured) {
u2f.abort(); u2f.abort();
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fState = ""; root.u2fState = "";
u2f.checkAvail(); u2f.checkAvail();
@@ -7,7 +7,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:notification-center-popout" layerNamespace: "dms:notification-center-popout"
fullHeightSurface: true fullHeightSurface: false
property bool notificationHistoryVisible: false property bool notificationHistoryVisible: false
property var triggerScreen: null property var triggerScreen: null
@@ -39,11 +39,9 @@ DankPopout {
} }
} }
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400 popupWidth: 400
popupHeight: stablePopupHeight popupHeight: stablePopupHeight
positioning: "" positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false suspendShadowWhileResizing: false
screen: triggerScreen screen: triggerScreen
@@ -24,6 +24,29 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1 property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0 property real _storedTopMargin: 0
property real _storedBottomMargin: 0 property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return content.height + entryTravel;
return content.width + entryTravel;
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss") readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -137,9 +160,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying enabled: !exiting && !_isDestroying
NumberAnimation { NumberAnimation {
id: implicitHeightAnim id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -911,9 +934,9 @@ PanelWindow {
if (isCenterPosition) if (isCenterPosition)
return 0; return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -entryTravel : entryTravel;
} }
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0 y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
} }
] ]
} }
@@ -925,16 +948,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x" property: isCenterPosition ? "y" : "x"
from: { from: {
if (isTopCenter) if (isTopCenter)
return -Anims.slidePx; return -entryTravel;
if (isBottomCenter) if (isBottomCenter)
return Anims.slidePx; return entryTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -entryTravel : entryTravel;
} }
to: 0 to: 0
duration: Theme.notificationEnterDuration duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantPopoutEnterCurve
onStopped: { onStopped: {
if (!win.exiting && !win._isDestroying) { if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) { if (isCenterPosition) {
@@ -959,35 +982,35 @@ PanelWindow {
from: 0 from: 0
to: { to: {
if (isTopCenter) if (isTopCenter)
return -Anims.slidePx; return -exitTravel;
if (isBottomCenter) if (isBottomCenter)
return Anims.slidePx; return exitTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -exitTravel : exitTravel;
} }
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "opacity" property: "opacity"
from: 1 from: 1
to: 0 to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standardAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "scale" property: "scale"
from: 1 from: 1
to: 0.98 to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
} }
@@ -108,13 +108,6 @@ QtObject {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData; return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
} }
function _isFocusedScreen() {
if (!SettingsData.notificationFocusedMonitor)
return true;
const focused = CompositorService.getFocusedScreen();
return focused && manager.modelData && focused.name === manager.modelData.name;
}
function _sync(newWrappers) { function _sync(newWrappers) {
for (const p of popupWindows.slice()) { for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting) if (!_isValidWindow(p) || p.exiting)
@@ -125,7 +118,7 @@ QtObject {
} }
} }
for (const w of newWrappers) { for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen()) if (w && !_hasWindowFor(w))
_insertAtTop(w); _insertAtTop(w);
} }
} }
@@ -909,9 +909,6 @@ Singleton {
case "dwl": case "dwl":
DwlService.generateOutputsConfig(outputsData); DwlService.generateOutputsConfig(outputsData);
break; break;
default:
WlrOutputService.applyOutputsConfig(outputsData, outputs);
break;
} }
} }
@@ -417,15 +417,6 @@ Item {
} }
} }
DankToggle {
width: parent.width
text: I18n.tr("Focused monitor only")
description: I18n.tr("Show notifications only on the currently focused monitor")
visible: parent.componentId === "notifications"
checked: SettingsData.notificationFocusedMonitor
onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked)
}
DankToggle { DankToggle {
width: parent.width width: parent.width
text: I18n.tr("Show on Last Display") text: I18n.tr("Show on Last Display")
-10
View File
@@ -160,16 +160,6 @@ Item {
onToggled: checked => SettingsData.set("dockGroupByApp", checked) onToggled: checked => SettingsData.set("dockGroupByApp", checked)
} }
SettingsToggleRow {
settingKey: "dockRestoreSpecialWorkspaceOnClick"
tags: ["dock", "hyprland", "special", "workspace", "restore"]
text: I18n.tr("Restore Special Workspace Windows")
description: I18n.tr("When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window")
checked: SettingsData.dockRestoreSpecialWorkspaceOnClick
visible: CompositorService.isHyprland
onToggled: checked => SettingsData.set("dockRestoreSpecialWorkspaceOnClick", checked)
}
SettingsButtonGroupRow { SettingsButtonGroupRow {
settingKey: "dockIndicatorStyle" settingKey: "dockIndicatorStyle"
tags: ["dock", "indicator", "style", "circle", "line"] tags: ["dock", "indicator", "style", "circle", "line"]
-784
View File
@@ -1,784 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
readonly property bool greeterFprintToggleAvailable: SettingsData.greeterFingerprintCanEnable || SettingsData.greeterEnableFprint
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
function greeterFingerprintDescription() {
const source = SettingsData.greeterFingerprintSource;
const reason = SettingsData.greeterFingerprintReason;
if (source === "pam") {
switch (reason) {
case "configured_externally":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.");
case "missing_enrollment":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet.");
case "missing_reader":
return I18n.tr("PAM provides fingerprint auth, but no reader was detected.");
default:
return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed.");
}
}
switch (reason) {
case "ready":
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.");
case "missing_enrollment":
if (SettingsData.greeterEnableFprint)
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.");
case "missing_reader":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.");
default:
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
}
}
function greeterU2fDescription() {
const source = SettingsData.greeterU2fSource;
const reason = SettingsData.greeterU2fReason;
if (source === "pam") {
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login.");
}
switch (reason) {
case "ready":
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Available.");
case "missing_key_registration":
if (SettingsData.greeterEnableU2f)
return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.");
default:
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
}
}
function refreshAuthDetection() {
SettingsData.refreshAuthAvailability();
}
onVisibleChanged: {
if (visible)
refreshAuthDetection();
}
ConfirmModal {
id: greeterActionConfirm
}
FileBrowserModal {
id: greeterWallpaperBrowserModal
browserTitle: I18n.tr("Select greeter background image")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"]
onFileSelected: path => {
SettingsData.set("greeterWallpaperPath", path);
close();
}
}
property string greeterStatusText: ""
property bool greeterStatusRunning: false
property bool greeterSyncRunning: false
property bool greeterInstallActionRunning: false
property string greeterStatusStdout: ""
property string greeterStatusStderr: ""
property string greeterSyncStdout: ""
property string greeterSyncStderr: ""
property string greeterSudoProbeStderr: ""
property string greeterTerminalFallbackStderr: ""
property bool greeterTerminalFallbackFromPrecheck: false
property var cachedFontFamilies: []
property bool fontsEnumerated: false
property bool greeterBinaryExists: false
property bool greeterEnabled: false
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
readonly property string greeterActionLabel: {
if (!root.greeterInstalled)
return I18n.tr("Install");
if (!root.greeterEnabled)
return I18n.tr("Activate");
return I18n.tr("Uninstall");
}
readonly property string greeterActionIcon: {
if (!root.greeterInstalled)
return "download";
if (!root.greeterEnabled)
return "login";
return "delete";
}
readonly property var greeterActionCommand: {
if (!root.greeterInstalled)
return ["dms", "greeter", "install", "--terminal"];
if (!root.greeterEnabled)
return ["dms", "greeter", "enable", "--terminal"];
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
}
property string greeterPendingAction: ""
function checkGreeterInstallState() {
greetdEnabledCheckProcess.running = true;
greeterBinaryCheckProcess.running = true;
}
function runGreeterStatus() {
greeterStatusText = "";
greeterStatusStdout = "";
greeterStatusStderr = "";
greeterStatusRunning = true;
greeterStatusProcess.running = true;
}
function runGreeterInstallAction() {
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true;
}
function promptGreeterActionConfirm() {
var title, message, confirmText;
if (!root.greeterInstalled) {
title = I18n.tr("Install Greeter", "greeter action confirmation");
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
confirmText = I18n.tr("Install");
} else if (!root.greeterEnabled) {
title = I18n.tr("Activate Greeter", "greeter action confirmation");
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
confirmText = I18n.tr("Activate");
} else {
title = I18n.tr("Uninstall Greeter", "greeter action confirmation");
message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication.");
confirmText = I18n.tr("Uninstall");
}
greeterActionConfirm.showWithOptions({
"title": title,
"message": message,
"confirmText": confirmText,
"cancelText": I18n.tr("Cancel"),
"confirmColor": Theme.primary,
"onConfirm": () => root.runGreeterInstallAction(),
"onCancel": () => {}
});
}
function runGreeterSync() {
greeterSyncStdout = "";
greeterSyncStderr = "";
greeterSudoProbeStderr = "";
greeterTerminalFallbackStderr = "";
greeterTerminalFallbackFromPrecheck = false;
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…");
greeterSyncRunning = true;
greeterSudoProbeProcess.running = true;
}
function launchGreeterSyncTerminalFallback(fromPrecheck, statusText) {
greeterTerminalFallbackFromPrecheck = fromPrecheck;
if (statusText && statusText !== "")
greeterStatusText = statusText;
greeterTerminalFallbackStderr = "";
greeterTerminalFallbackProcess.running = true;
}
function enumerateFonts() {
if (fontsEnumerated)
return;
var fonts = [];
var availableFonts = Qt.fontFamilies();
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i];
if (fontName.startsWith("."))
continue;
fonts.push(fontName);
}
fonts.sort();
fonts.unshift("Default");
cachedFontFamilies = fonts;
fontsEnumerated = true;
}
Component.onCompleted: {
refreshAuthDetection();
Qt.callLater(enumerateFonts);
Qt.callLater(checkGreeterInstallState);
}
Process {
id: greetdEnabledCheckProcess
command: ["systemctl", "is-enabled", "greetd"]
running: false
stdout: StdioCollector {
onStreamFinished: root.greeterEnabled = text.trim() === "enabled"
}
}
Process {
id: greeterBinaryCheckProcess
command: ["sh", "-c", "test -f /usr/bin/dms-greeter || test -f /usr/local/bin/dms-greeter"]
running: false
onExited: exitCode => {
root.greeterBinaryExists = (exitCode === 0);
}
}
Process {
id: greeterStatusProcess
command: ["dms", "greeter", "status"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.greeterStatusStdout = text || "";
}
}
stderr: StdioCollector {
onStreamFinished: root.greeterStatusStderr = text || ""
}
onExited: exitCode => {
root.greeterStatusRunning = false;
const out = (root.greeterStatusStdout || "").trim();
const err = (root.greeterStatusStderr || "").trim();
if (exitCode === 0) {
root.greeterStatusText = out !== "" ? out : I18n.tr("No status output.");
if (err !== "")
root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err;
return;
}
var failure = I18n.tr("Failed to run 'dms greeter status'. Ensure DMS is installed and dms is in PATH.", "greeter status error") + " (exit " + exitCode + ")";
if (out !== "")
failure = failure + "\n\n" + out;
if (err !== "")
failure = failure + "\n\nstderr:\n" + err;
root.greeterStatusText = failure;
}
}
Process {
id: greeterSyncProcess
command: ["dms", "greeter", "sync", "--yes"]
running: false
stdout: StdioCollector {
onStreamFinished: root.greeterSyncStdout = text || ""
}
stderr: StdioCollector {
onStreamFinished: root.greeterSyncStderr = text || ""
}
onExited: exitCode => {
root.greeterSyncRunning = false;
const out = (root.greeterSyncStdout || "").trim();
const err = (root.greeterSyncStderr || "").trim();
if (exitCode === 0) {
var success = I18n.tr("Sync completed successfully.");
if (out !== "")
success = success + "\n\n" + out;
if (err !== "")
success = success + "\n\nstderr:\n" + err;
root.greeterStatusText = success;
} else {
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
if (out !== "")
failure = failure + "\n\n" + out;
if (err !== "")
failure = failure + "\n\nstderr:\n" + err;
root.greeterStatusText = failure;
root.launchGreeterSyncTerminalFallback(false, "");
}
root.checkGreeterInstallState();
}
}
Process {
id: greeterSudoProbeProcess
command: ["sudo", "-n", "true"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterSudoProbeStderr = text || ""
}
onExited: exitCode => {
const err = (root.greeterSudoProbeStderr || "").trim();
if (exitCode === 0) {
root.greeterStatusText = I18n.tr("Running greeter sync…");
greeterSyncProcess.running = true;
return;
}
var authNeeded = I18n.tr("Sync needs sudo authentication. Opening terminal so you can use password or fingerprint.");
if (err !== "")
authNeeded = authNeeded + "\n\n" + err;
root.launchGreeterSyncTerminalFallback(true, authNeeded);
}
}
Process {
id: greeterTerminalFallbackProcess
command: ["dms", "greeter", "sync", "--terminal", "--yes"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterTerminalFallbackStderr = text || ""
}
onExited: exitCode => {
root.greeterSyncRunning = false;
if (exitCode === 0) {
var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done.");
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched;
return;
}
var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")";
const err = (root.greeterTerminalFallbackStderr || "").trim();
if (err !== "")
fallback = fallback + "\n\nstderr:\n" + err;
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + fallback : fallback;
}
}
Process {
id: greeterInstallActionProcess
command: root.greeterActionCommand
running: false
onExited: exitCode => {
root.greeterInstallActionRunning = false;
const pending = root.greeterPendingAction;
root.greeterPendingAction = "";
if (exitCode === 0) {
if (pending === "install")
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
else if (pending === "activate")
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
else
root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed.");
} else {
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
}
root.checkGreeterInstallState();
}
}
readonly property var _lockDateFormatPresets: [
{
format: "",
label: I18n.tr("System Default", "date format option")
},
{
format: "ddd d",
label: I18n.tr("Day Date", "date format option")
},
{
format: "ddd MMM d",
label: I18n.tr("Day Month Date", "date format option")
},
{
format: "MMM d",
label: I18n.tr("Month Date", "date format option")
},
{
format: "M/d",
label: I18n.tr("Numeric (M/D)", "date format option")
},
{
format: "d/M",
label: I18n.tr("Numeric (D/M)", "date format option")
},
{
format: "ddd d MMM yyyy",
label: I18n.tr("Full with Year", "date format option")
},
{
format: "yyyy-MM-dd",
label: I18n.tr("ISO Date", "date format option")
},
{
format: "dddd, MMMM d",
label: I18n.tr("Full Day & Month", "date format option")
}
]
readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
width: parent.width
iconName: "info"
title: I18n.tr("Greeter Status")
settingKey: "greeterStatus"
StyledText {
text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
Item {
width: 1
height: Theme.spacingS
}
Rectangle {
width: parent.width
height: Math.min(180, statusTextArea.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
StyledText {
id: statusTextArea
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.greeterStatusRunning ? I18n.tr("Checking…", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
Item {
width: 1
height: Theme.spacingM
}
RowLayout {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: root.greeterActionLabel
iconName: root.greeterActionIcon
horizontalPadding: Theme.spacingL
onClicked: root.promptGreeterActionConfirm()
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
}
Item {
Layout.fillWidth: true
}
DankButton {
text: I18n.tr("Refresh")
iconName: "refresh"
horizontalPadding: Theme.spacingL
onClicked: root.runGreeterStatus()
enabled: !root.greeterStatusRunning
}
DankButton {
text: I18n.tr("Sync")
iconName: "sync"
horizontalPadding: Theme.spacingL
onClicked: root.runGreeterSync()
enabled: root.greeterInstalled && !root.greeterSyncRunning && !root.greeterInstallActionRunning
}
}
}
SettingsCard {
width: parent.width
iconName: "fingerprint"
title: I18n.tr("Login Authentication")
settingKey: "greeterAuth"
StyledText {
text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
SettingsToggleRow {
settingKey: "greeterEnableFprint"
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
text: I18n.tr("Enable fingerprint at login")
description: root.greeterFingerprintDescription()
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableFprint
enabled: root.greeterFprintToggleAvailable
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
}
SettingsToggleRow {
settingKey: "greeterEnableU2f"
tags: ["greeter", "u2f", "security", "key", "login", "auth"]
text: I18n.tr("Enable security key at login")
description: root.greeterU2fDescription()
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableU2f
enabled: root.greeterU2fToggleAvailable
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
}
}
SettingsCard {
width: parent.width
iconName: "palette"
title: I18n.tr("Greeter Appearance")
settingKey: "greeterAppearance"
StyledText {
text: I18n.tr("Font")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
SettingsDropdownRow {
settingKey: "greeterFontFamily"
tags: ["greeter", "font", "typography"]
text: I18n.tr("Greeter font")
description: I18n.tr("Font used on the login screen")
options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"]
currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default")
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
onValueChanged: value => {
if (value === "Default")
SettingsData.set("greeterFontFamily", "");
else
SettingsData.set("greeterFontFamily", value);
}
}
StyledText {
text: I18n.tr("Time format")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
SettingsToggleRow {
settingKey: "greeterUse24Hour"
tags: ["greeter", "time", "24hour"]
text: I18n.tr("24-hour clock")
description: I18n.tr("Greeter only — does not affect main clock")
checked: SettingsData.greeterUse24HourClock
onToggled: checked => SettingsData.set("greeterUse24HourClock", checked)
}
SettingsToggleRow {
settingKey: "greeterShowSeconds"
tags: ["greeter", "time", "seconds"]
text: I18n.tr("Show seconds")
checked: SettingsData.greeterShowSeconds
onToggled: checked => SettingsData.set("greeterShowSeconds", checked)
}
SettingsToggleRow {
settingKey: "greeterPadHours"
tags: ["greeter", "time", "12hour"]
text: I18n.tr("Pad hours (02:00 vs 2:00)")
visible: !SettingsData.greeterUse24HourClock
checked: SettingsData.greeterPadHours12Hour
onToggled: checked => SettingsData.set("greeterPadHours12Hour", checked)
}
StyledText {
text: I18n.tr("Date format on greeter")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
SettingsDropdownRow {
settingKey: "greeterLockDateFormat"
tags: ["greeter", "date", "format"]
text: I18n.tr("Date format")
description: I18n.tr("Greeter only — format for the date on the login screen")
options: root._lockDateFormatPresets.map(p => p.label)
currentValue: {
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || "";
var match = root._lockDateFormatPresets.find(p => p.format === current);
return match ? match.label : (current ? I18n.tr("Custom: ") + current : root._lockDateFormatPresets[0].label);
}
onValueChanged: value => {
var preset = root._lockDateFormatPresets.find(p => p.label === value);
SettingsData.set("greeterLockDateFormat", preset ? preset.format : "");
}
}
StyledText {
text: I18n.tr("Background")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
StyledText {
text: I18n.tr("Use a custom image for the login screen, or leave empty to use your desktop wallpaper.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
Row {
width: parent.width
spacing: Theme.spacingS
DankTextField {
id: greeterWallpaperPathField
width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS
placeholderText: I18n.tr("Use desktop wallpaper")
text: SettingsData.greeterWallpaperPath
backgroundColor: Theme.surfaceContainerHighest
onTextChanged: {
if (text !== SettingsData.greeterWallpaperPath)
SettingsData.set("greeterWallpaperPath", text);
}
}
DankButton {
id: browseGreeterWallpaperButton
text: I18n.tr("Browse")
horizontalPadding: Theme.spacingL
onClicked: greeterWallpaperBrowserModal.open()
}
}
SettingsDropdownRow {
settingKey: "greeterWallpaperFillMode"
tags: ["greeter", "wallpaper", "background", "fill"]
text: I18n.tr("Wallpaper fill mode")
description: I18n.tr("How the background image is scaled")
options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode"))
currentValue: {
var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill");
var idx = root._wallpaperFillModes.indexOf(mode);
return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode");
}
onValueChanged: value => {
var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value);
if (idx >= 0)
SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]);
}
}
StyledText {
text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
}
SettingsCard {
width: parent.width
iconName: "history"
title: I18n.tr("Greeter Behavior")
settingKey: "greeterBehavior"
StyledText {
text: I18n.tr("Convenience options for the login screen. Sync to apply.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
SettingsToggleRow {
settingKey: "greeterRememberLastSession"
tags: ["greeter", "session", "remember", "login"]
text: I18n.tr("Remember last session")
description: I18n.tr("Pre-select the last used session on the greeter")
checked: SettingsData.greeterRememberLastSession
onToggled: checked => SettingsData.set("greeterRememberLastSession", checked)
}
SettingsToggleRow {
settingKey: "greeterRememberLastUser"
tags: ["greeter", "user", "remember", "login", "username"]
text: I18n.tr("Remember last user")
description: I18n.tr("Pre-fill the last successful username on the greeter")
checked: SettingsData.greeterRememberLastUser
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
}
}
SettingsCard {
width: parent.width
iconName: "extension"
title: I18n.tr("Dependencies & documentation")
settingKey: "greeterDeps"
StyledText {
text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
StyledText {
text: I18n.tr("Installation and PAM setup: see the ") + "<a href=\"https://danklinux.com/docs/dankgreeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">DankGreeter docs</a> " + I18n.tr("or run ") + "'dms greeter install'."
textFormat: Text.RichText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
linkColor: Theme.primary
width: parent.width
wrapMode: Text.Wrap
onLinkActivated: url => Qt.openUrlExternally(url)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
}
}
}
+8 -53
View File
@@ -9,51 +9,6 @@ import qs.Modules.Settings.Widgets
Item { Item {
id: root id: root
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
function lockFingerprintDescription() {
switch (SettingsData.lockFingerprintReason) {
case "ready":
return I18n.tr("Use fingerprint authentication for the lock screen.");
case "missing_enrollment":
if (SettingsData.enableFprint)
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
case "missing_reader":
return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd.");
default:
return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
}
}
function lockU2fDescription() {
switch (SettingsData.lockU2fReason) {
case "ready":
return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
case "missing_key_registration":
if (SettingsData.enableU2f)
return I18n.tr("Enabled, but no registered security key was found yet. Register a key or update your U2F config.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f.");
default:
return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
}
}
function refreshAuthDetection() {
SettingsData.refreshAuthAvailability();
}
Component.onCompleted: refreshAuthDetection()
onVisibleChanged: {
if (visible)
refreshAuthDetection();
}
FileBrowserModal { FileBrowserModal {
id: videoBrowserModal id: videoBrowserModal
browserTitle: I18n.tr("Select Video or Folder") browserTitle: I18n.tr("Select Video or Folder")
@@ -217,10 +172,10 @@ Item {
settingKey: "enableFprint" settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"] tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
text: I18n.tr("Enable fingerprint authentication") text: I18n.tr("Enable fingerprint authentication")
description: root.lockFingerprintDescription() description: SettingsData.fprintdAvailable ? I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)") : I18n.tr("Not enrolled", "fingerprint not detected status")
descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableFprint checked: SettingsData.enableFprint
enabled: root.lockFprintToggleAvailable enabled: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.set("enableFprint", checked) onToggled: checked => SettingsData.set("enableFprint", checked)
} }
@@ -228,10 +183,10 @@ Item {
settingKey: "enableU2f" settingKey: "enableU2f"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"] tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen") text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
description: root.lockU2fDescription() description: SettingsData.u2fAvailable ? I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting") : I18n.tr("Not enrolled", "security key not detected status")
descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableU2f checked: SettingsData.enableU2f
enabled: root.lockU2fToggleAvailable enabled: SettingsData.u2fAvailable
onToggled: checked => SettingsData.set("enableU2f", checked) onToggled: checked => SettingsData.set("enableU2f", checked)
} }
@@ -240,7 +195,7 @@ Item {
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"] tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting") text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting") description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
visible: SettingsData.enableU2f visible: SettingsData.u2fAvailable && SettingsData.enableU2f
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")] options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method") currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
onValueChanged: value => { onValueChanged: value => {
@@ -290,7 +245,7 @@ Item {
StyledText { StyledText {
text: I18n.tr("Path to a video file or folder containing videos") text: I18n.tr("Path to a video file or folder containing videos")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeXSmall
color: Theme.outlineVariant color: Theme.outlineVariant
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
+5 -10
View File
@@ -1935,6 +1935,11 @@ Item {
label: I18n.tr("Auth Type"), label: I18n.tr("Auth Type"),
value: data["connection-type"] value: data["connection-type"]
}); });
fields.push({
label: I18n.tr("Autoconnect"),
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
return fields; return fields;
} }
@@ -1973,16 +1978,6 @@ Item {
} }
} }
DankToggle {
width: parent.width
text: I18n.tr("Autoconnect")
checked: configData ? (configData.autoconnect || false) : false
visible: !VPNService.configLoading && configData !== null
onToggled: checked => {
VPNService.updateConfig(modelData.uuid, {autoconnect: checked});
}
}
Item { Item {
width: 1 width: 1
height: Theme.spacingXS height: Theme.spacingXS
@@ -20,10 +20,7 @@ Item {
var out = []; var out = [];
for (var i = 0; i < rules.length; i++) { for (var i = 0; i < rules.length; i++) {
if ((rules[i].action || "").toString().toLowerCase() === "mute") if ((rules[i].action || "").toString().toLowerCase() === "mute")
out.push({ out.push({ rule: rules[i], index: i });
rule: rules[i],
index: i
});
} }
return out; return out;
} }
@@ -291,15 +288,6 @@ Item {
onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked) onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked)
} }
SettingsToggleRow {
settingKey: "notificationFocusedMonitor"
tags: ["notification", "popup", "focused", "monitor", "display", "screen", "active"]
text: I18n.tr("Focused Monitor Only")
description: I18n.tr("Show notification popups only on the currently focused monitor")
checked: SettingsData.notificationFocusedMonitor
onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked)
}
Item { Item {
width: parent.width width: parent.width
height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2 height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2
@@ -352,7 +340,6 @@ Item {
} }
SettingsSliderRow { SettingsSliderRow {
id: animationDurationSlider
settingKey: "notificationCustomAnimationDuration" settingKey: "notificationCustomAnimationDuration"
tags: ["notification", "animation", "duration", "custom", "speed"] tags: ["notification", "animation", "duration", "custom", "speed"]
text: I18n.tr("Duration") text: I18n.tr("Duration")
@@ -368,13 +355,6 @@ Item {
} }
SettingsData.set("notificationCustomAnimationDuration", newValue); SettingsData.set("notificationCustomAnimationDuration", newValue);
} }
Connections {
target: Theme
function onNotificationAnimationBaseDurationChanged() {
animationDurationSlider.value = Theme.notificationAnimationBaseDuration;
}
}
} }
} }
} }
@@ -408,8 +408,6 @@ FloatingWindow {
} }
clip: true clip: true
visible: !root.isLoading visible: !root.isLoading
add: null
displaced: null
ScrollBar.vertical: DankScrollbar { ScrollBar.vertical: DankScrollbar {
id: browserScrollbar id: browserScrollbar
+21 -46
View File
@@ -2,6 +2,7 @@ import QtCore
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -750,24 +751,6 @@ Item {
} }
} }
StyledText {
text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
visible: themeColorsTab.installedRegistryThemes.length === 0
horizontalAlignment: Text.AlignHCenter
}
DankButton {
text: I18n.tr("Browse Themes", "browse themes button")
iconName: "store"
anchors.horizontalCenter: parent.horizontalCenter
onClicked: showThemeBrowser()
}
}
Column { Column {
id: variantSelector id: variantSelector
width: parent.width width: parent.width
@@ -775,9 +758,7 @@ Item {
visible: activeThemeId !== "" && activeThemeVariants !== null && (isMultiVariant || (activeThemeVariants.options && activeThemeVariants.options.length > 0)) visible: activeThemeId !== "" && activeThemeVariants !== null && (isMultiVariant || (activeThemeVariants.options && activeThemeVariants.options.length > 0))
property string activeThemeId: { property string activeThemeId: {
switch (Theme.currentThemeCategory) { if (Theme.currentThemeCategory !== "registry" || Theme.currentTheme !== "custom")
case "registry":
if (Theme.currentTheme !== "custom")
return ""; return "";
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i]; var t = themeColorsTab.installedRegistryThemes[i];
@@ -785,28 +766,16 @@ Item {
return t.id; return t.id;
} }
return ""; return "";
case "custom":
return Theme.currentThemeId || "";
default:
return "";
}
} }
property var activeThemeVariants: { property var activeThemeVariants: {
if (!activeThemeId) if (!activeThemeId)
return null; return null;
switch (Theme.currentThemeCategory) {
case "registry":
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) { for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i]; var t = themeColorsTab.installedRegistryThemes[i];
if (t.id === activeThemeId && t.hasVariants) if (t.id === activeThemeId && t.hasVariants)
return t.variants; return t.variants;
} }
return null; return null;
case "custom":
return Theme.currentThemeVariants || null;
default:
return null;
}
} }
property bool isMultiVariant: activeThemeVariants?.type === "multi" property bool isMultiVariant: activeThemeVariants?.type === "multi"
property string colorMode: Theme.isLightMode ? "light" : "dark" property string colorMode: Theme.isLightMode ? "light" : "dark"
@@ -923,7 +892,7 @@ Item {
width: accentColorsGrid.dotSize width: accentColorsGrid.dotSize
height: accentColorsGrid.dotSize height: accentColorsGrid.dotSize
radius: width / 2 radius: width / 2
color: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary color: modelData.color || Theme.primary
border.color: Theme.outline border.color: Theme.outline
border.width: isSelected ? 2 : 1 border.width: isSelected ? 2 : 1
scale: isSelected ? 1.1 : 1 scale: isSelected ? 1.1 : 1
@@ -1009,6 +978,24 @@ Item {
} }
} }
} }
StyledText {
text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
visible: themeColorsTab.installedRegistryThemes.length === 0
horizontalAlignment: Text.AlignHCenter
}
DankButton {
text: I18n.tr("Browse Themes", "browse themes button")
iconName: "store"
anchors.horizontalCenter: parent.horizontalCenter
onClicked: showThemeBrowser()
}
}
} }
} }
@@ -2651,18 +2638,6 @@ Item {
checked: SettingsData.matugenTemplateEmacs checked: SettingsData.matugenTemplateEmacs
onToggled: checked => SettingsData.set("matugenTemplateEmacs", checked) onToggled: checked => SettingsData.set("matugenTemplateEmacs", checked)
} }
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "zed", "template"]
settingKey: "matugenTemplateZed"
text: "Zed"
description: getTemplateDescription("zed", "")
descriptionColor: getTemplateDescriptionColor("zed")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateZed
onToggled: checked => SettingsData.set("matugenTemplateZed", checked)
}
} }
Rectangle { Rectangle {
@@ -55,6 +55,180 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard {
tab: "typography"
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
title: I18n.tr("Animation Style")
settingKey: "animationVariant"
iconName: "auto_awesome_motion"
Item {
width: parent.width
height: animVariantGroup.implicitHeight
clip: true
DankButtonGroup {
id: animVariantGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
selectionMode: "single"
currentIndex: SettingsData.animationVariant
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("animationVariant", index);
}
Connections {
target: SettingsData
function onAnimationVariantChanged() {
animVariantGroup.currentIndex = SettingsData.animationVariant;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: variantDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: variantDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.animationVariant) {
case 1:
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
case 2:
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
default:
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
}
}
}
}
}
SettingsCard {
tab: "typography"
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
title: I18n.tr("Motion Effects")
settingKey: "motionEffect"
iconName: "motion_photos_on"
Item {
width: parent.width
height: motionEffectGroup.implicitHeight
clip: true
DankButtonGroup {
id: motionEffectGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
selectionMode: "single"
currentIndex: SettingsData.motionEffect
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("motionEffect", index);
}
Connections {
target: SettingsData
function onMotionEffectChanged() {
motionEffectGroup.currentIndex = SettingsData.motionEffect;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: motionEffectDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.motionEffect) {
case 1:
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
case 2:
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
default:
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
visible: SettingsData.motionEffect === 1
}
SettingsDropdownRow {
visible: SettingsData.motionEffect === 1
tab: "typography"
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll"]
settingKey: "directionalAnimationMode"
text: I18n.tr("Directional Behavior")
description: I18n.tr("How the popout emerges from the DankBar")
options: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
currentValue: {
switch (SettingsData.directionalAnimationMode) {
case 1:
return I18n.tr("Slide");
case 2:
return I18n.tr("Roll");
default:
return I18n.tr("Overlap");
}
}
onValueChanged: value => {
if (value === I18n.tr("Slide"))
SettingsData.set("directionalAnimationMode", 1);
else if (value === I18n.tr("Roll"))
SettingsData.set("directionalAnimationMode", 2);
else
SettingsData.set("directionalAnimationMode", 0);
}
}
}
SettingsCard { SettingsCard {
tab: "typography" tab: "typography"
tags: ["font", "family", "text", "typography"] tags: ["font", "family", "text", "typography"]
@@ -125,16 +125,6 @@ Item {
onToggled: checked => SettingsData.set("groupWorkspaceApps", checked) onToggled: checked => SettingsData.set("groupWorkspaceApps", checked)
} }
SettingsToggleRow {
settingKey: "workspaceActiveAppHighlightEnabled"
tags: ["workspace", "apps", "icons", "highlight", "active", "focused"]
text: I18n.tr("Highlight Active Workspace App")
description: I18n.tr("Highlight the currently focused app inside workspace indicators")
checked: SettingsData.workspaceActiveAppHighlightEnabled
visible: SettingsData.showWorkspaceApps
onToggled: checked => SettingsData.set("workspaceActiveAppHighlightEnabled", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "workspaceFollowFocus" settingKey: "workspaceFollowFocus"
tags: ["workspace", "focus", "follow", "monitor"] tags: ["workspace", "focus", "follow", "monitor"]
@@ -121,9 +121,9 @@ Scope {
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
@@ -154,45 +154,69 @@ Scope {
id: scaleTransform id: scaleTransform
origin.x: contentContainer.width / 2 origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2 origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : 0.96 xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
yScale: overviewScope.overviewOpen ? 1 : 0.96 yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
Behavior on xScale { Behavior on xScale {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
Behavior on yScale { Behavior on yScale {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
} }
Translate { Translate {
id: motionTransform id: motionTransform
x: 0 x: {
y: overviewScope.overviewOpen ? 0 : Theme.spacingL if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return 0;
if (Theme.isDepthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
y: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
if (Theme.isDepthEffect)
return Math.max(Theme.effectAnimOffset * 0.85, 28);
return Theme.effectAnimOffset;
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
} }
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
@@ -202,8 +202,18 @@ Scope {
Item { Item {
id: spotlightContainer id: spotlightContainer
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr) readonly property bool directionalEffect: Theme.isDirectionalEffect
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr) readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
readonly property real collapsedMotionY: {
if (directionalEffect)
return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1);
if (depthEffect)
return Math.max(Theme.effectAnimOffset * 0.8, 30);
return 0;
}
x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr)
y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr)
readonly property int baseWidth: { readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) { switch (SettingsData.dankLauncherV2Size) {
@@ -234,8 +244,8 @@ Scope {
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
visible: overlayWindow.shouldShowSpotlight || animatingOut visible: overlayWindow.shouldShowSpotlight || animatingOut
enabled: overlayWindow.shouldShowSpotlight enabled: overlayWindow.shouldShowSpotlight
@@ -245,10 +255,11 @@ Scope {
Behavior on scale { Behavior on scale {
id: scaleAnimation id: scaleAnimation
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.fast duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
onRunningChanged: { onRunningChanged: {
if (running || !spotlightContainer.animatingOut) if (running || !spotlightContainer.animatingOut)
return; return;
@@ -258,10 +269,27 @@ Scope {
} }
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.fast duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
@@ -62,30 +62,30 @@ Item {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
@@ -124,16 +124,16 @@ Item {
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantModalEnterCurve
} }
} }
} }
+1 -3
View File
@@ -27,9 +27,7 @@ quickshell -p quickshell/
**Code formatting:** **Code formatting:**
```bash ```bash
qmlfmt -t 4 -i 4 -b 250 -w path/to/file.qml qmlfmt -t 4 -i 4 -b 250 -w path/to/file.qml
make lint-qml # Run from repo root; requires quickshell/.qmlls.ini (generated by `qs -p quickshell/`) qmllint **/*.qml
# Uses Qt 6 qmllint. Override path with QMLLINT=/path/to/qmllint if needed.
# Auto-detects `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint`.
``` ```
## Components ## Components
-1
View File
@@ -4,7 +4,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common
Singleton { Singleton {
id: root id: root
+6 -6
View File
@@ -93,9 +93,9 @@ Singleton {
`; `;
monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor"); monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor");
monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout > 0 ? root.monitorTimeout : 86400);
monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0); monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0);
monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout);
monitorOffMonitor.isIdleChanged.connect(function () { monitorOffMonitor.isIdleChanged.connect(function () {
if (monitorOffMonitor.isIdle) { if (monitorOffMonitor.isIdle) {
if (SettingsData.fadeToDpmsEnabled) { if (SettingsData.fadeToDpmsEnabled) {
@@ -112,9 +112,9 @@ Singleton {
}); });
lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor"); lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor");
lockMonitor.timeout = Qt.binding(() => root.lockTimeout > 0 ? root.lockTimeout : 86400);
lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0); lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0);
lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
lockMonitor.timeout = Qt.binding(() => root.lockTimeout);
lockMonitor.isIdleChanged.connect(function () { lockMonitor.isIdleChanged.connect(function () {
if (lockMonitor.isIdle) { if (lockMonitor.isIdle) {
if (SettingsData.fadeToLockEnabled) { if (SettingsData.fadeToLockEnabled) {
@@ -130,9 +130,9 @@ Singleton {
}); });
suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor"); suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor");
suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout > 0 ? root.suspendTimeout : 86400);
suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0); suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0);
suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout);
suspendMonitor.isIdleChanged.connect(function () { suspendMonitor.isIdleChanged.connect(function () {
if (suspendMonitor.isIdle) { if (suspendMonitor.isIdle) {
root.requestSuspend(); root.requestSuspend();
-1
View File
@@ -142,7 +142,6 @@ Singleton {
} }
ToastService.showInfo(I18n.tr("VPN configuration updated")); ToastService.showInfo(I18n.tr("VPN configuration updated"));
DMSNetworkService.refreshVpnProfiles(); DMSNetworkService.refreshVpnProfiles();
getConfig(uuid);
configUpdated(); configUpdated();
}); });
} }
@@ -264,7 +264,7 @@ Singleton {
} }
if (process) { if (process) {
process.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`]; process.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
process.targetScreenName = screenName; process.targetScreenName = screenName;
process.currentWallpaper = currentWallpaper; process.currentWallpaper = currentWallpaper;
process.goToPrevious = false; process.goToPrevious = false;
@@ -272,7 +272,7 @@ Singleton {
} }
} else { } else {
// Use global process for fallback // Use global process for fallback
cyclingProcess.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`]; cyclingProcess.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
cyclingProcess.targetScreenName = screenName || ""; cyclingProcess.targetScreenName = screenName || "";
cyclingProcess.currentWallpaper = currentWallpaper; cyclingProcess.currentWallpaper = currentWallpaper;
cyclingProcess.running = true; cyclingProcess.running = true;
@@ -296,7 +296,7 @@ Singleton {
} }
if (process) { if (process) {
process.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`]; process.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
process.targetScreenName = screenName; process.targetScreenName = screenName;
process.currentWallpaper = currentWallpaper; process.currentWallpaper = currentWallpaper;
process.goToPrevious = true; process.goToPrevious = true;
@@ -304,7 +304,7 @@ Singleton {
} }
} else { } else {
// Use global process for fallback // Use global process for fallback
prevCyclingProcess.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`]; prevCyclingProcess.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
prevCyclingProcess.targetScreenName = screenName || ""; prevCyclingProcess.targetScreenName = screenName || "";
prevCyclingProcess.currentWallpaper = currentWallpaper; prevCyclingProcess.currentWallpaper = currentWallpaper;
prevCyclingProcess.running = true; prevCyclingProcess.running = true;
+91 -162
View File
@@ -18,191 +18,197 @@ Singleton {
target: DMSService target: DMSService
function onCapabilitiesReceived() { function onCapabilitiesReceived() {
checkCapabilities(); checkCapabilities()
} }
function onConnectionStateChanged() { function onConnectionStateChanged() {
if (DMSService.isConnected) { if (DMSService.isConnected) {
checkCapabilities(); checkCapabilities()
return; return
} }
wlrOutputAvailable = false; wlrOutputAvailable = false
} }
function onWlrOutputStateUpdate(data) { function onWlrOutputStateUpdate(data) {
if (!wlrOutputAvailable) { if (!wlrOutputAvailable) {
return; return
} }
handleStateUpdate(data); handleStateUpdate(data)
} }
} }
Component.onCompleted: { Component.onCompleted: {
if (!DMSService.dmsAvailable) { if (!DMSService.dmsAvailable) {
return; return
} }
checkCapabilities(); checkCapabilities()
} }
function checkCapabilities() { function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) { if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
wlrOutputAvailable = false; wlrOutputAvailable = false
return; return
} }
const hasWlrOutput = DMSService.capabilities.includes("wlroutput"); const hasWlrOutput = DMSService.capabilities.includes("wlroutput")
if (hasWlrOutput && !wlrOutputAvailable) { if (hasWlrOutput && !wlrOutputAvailable) {
wlrOutputAvailable = true; wlrOutputAvailable = true
console.info("WlrOutputService: wlr-output-management capability detected"); console.info("WlrOutputService: wlr-output-management capability detected")
requestState(); requestState()
return; return
} }
if (!hasWlrOutput) { if (!hasWlrOutput) {
wlrOutputAvailable = false; wlrOutputAvailable = false
} }
} }
function requestState() { function requestState() {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
return; return
} }
DMSService.sendRequest("wlroutput.getState", null, response => { DMSService.sendRequest("wlroutput.getState", null, response => {
if (!response.result) { if (!response.result) {
return; return
} }
handleStateUpdate(response.result); handleStateUpdate(response.result)
}); })
} }
function handleStateUpdate(state) { function handleStateUpdate(state) {
outputs = state.outputs || []; outputs = state.outputs || []
serial = state.serial || 0; serial = state.serial || 0
if (outputs.length === 0) { if (outputs.length === 0) {
console.warn("WlrOutputService: Received empty outputs list"); console.warn("WlrOutputService: Received empty outputs list")
} else { } else {
console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial); console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial)
outputs.forEach((output, index) => { outputs.forEach((output, index) => {
console.log("WlrOutputService: Output", index, "-", output.name, "enabled:", output.enabled, "mode:", output.currentMode ? output.currentMode.width + "x" + output.currentMode.height + "@" + (output.currentMode.refresh / 1000) + "Hz" : "none"); console.log("WlrOutputService: Output", index, "-", output.name,
}); "enabled:", output.enabled,
"mode:", output.currentMode ?
output.currentMode.width + "x" + output.currentMode.height + "@" +
(output.currentMode.refresh / 1000) + "Hz" : "none")
})
} }
stateChanged(); stateChanged()
} }
function getOutput(name) { function getOutput(name) {
for (const output of outputs) { for (const output of outputs) {
if (output.name === name) { if (output.name === name) {
return output; return output
} }
} }
return null; return null
} }
function getEnabledOutputs() { function getEnabledOutputs() {
return outputs.filter(output => output.enabled); return outputs.filter(output => output.enabled)
} }
function applyConfiguration(heads, callback) { function applyConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) { if (callback) {
callback(false, "Not connected"); callback(false, "Not connected")
} }
return; return
} }
console.log("WlrOutputService: Applying configuration for", heads.length, "outputs"); console.log("WlrOutputService: Applying configuration for", heads.length, "outputs")
heads.forEach((head, index) => { heads.forEach((head, index) => {
console.log("WlrOutputService: Head", index, "- name:", head.name, "enabled:", head.enabled, "modeId:", head.modeId, "customMode:", JSON.stringify(head.customMode), "position:", JSON.stringify(head.position), "scale:", head.scale, "transform:", head.transform, "adaptiveSync:", head.adaptiveSync); console.log("WlrOutputService: Head", index, "- name:", head.name,
}); "enabled:", head.enabled,
"modeId:", head.modeId,
"customMode:", JSON.stringify(head.customMode),
"position:", JSON.stringify(head.position),
"scale:", head.scale,
"transform:", head.transform,
"adaptiveSync:", head.adaptiveSync)
})
DMSService.sendRequest("wlroutput.applyConfiguration", { DMSService.sendRequest("wlroutput.applyConfiguration", {
"heads": heads "heads": heads
}, response => { }, response => {
const success = !response.error; const success = !response.error
const message = response.error || response.result?.message || ""; const message = response.error || response.result?.message || ""
if (response.error) { if (response.error) {
console.warn("WlrOutputService: applyConfiguration error:", response.error); console.warn("WlrOutputService: applyConfiguration error:", response.error)
} else { } else {
console.log("WlrOutputService: Configuration applied successfully"); console.log("WlrOutputService: Configuration applied successfully")
} }
configurationApplied(success, message); configurationApplied(success, message)
if (callback) { if (callback) {
callback(success, message); callback(success, message)
} }
}); })
} }
function testConfiguration(heads, callback) { function testConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) { if (callback) {
callback(false, "Not connected"); callback(false, "Not connected")
} }
return; return
} }
console.log("WlrOutputService: Testing configuration for", heads.length, "outputs"); console.log("WlrOutputService: Testing configuration for", heads.length, "outputs")
DMSService.sendRequest("wlroutput.testConfiguration", { DMSService.sendRequest("wlroutput.testConfiguration", {
"heads": heads "heads": heads
}, response => { }, response => {
const success = !response.error; const success = !response.error
const message = response.error || response.result?.message || ""; const message = response.error || response.result?.message || ""
if (response.error) { if (response.error) {
console.warn("WlrOutputService: testConfiguration error:", response.error); console.warn("WlrOutputService: testConfiguration error:", response.error)
} else { } else {
console.log("WlrOutputService: Configuration test passed"); console.log("WlrOutputService: Configuration test passed")
} }
if (callback) { if (callback) {
callback(success, message); callback(success, message)
} }
}); })
} }
function setOutputEnabled(outputName, enabled, callback) { function setOutputEnabled(outputName, enabled, callback) {
const output = getOutput(outputName); const output = getOutput(outputName)
if (!output) { if (!output) {
console.warn("WlrOutputService: Output not found:", outputName); console.warn("WlrOutputService: Output not found:", outputName)
if (callback) { if (callback) {
callback(false, "Output not found"); callback(false, "Output not found")
} }
return; return
} }
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": enabled "enabled": enabled
} }]
];
if (enabled && output.currentMode) { if (enabled && output.currentMode) {
heads[0].modeId = output.currentMode.id; heads[0].modeId = output.currentMode.id
} }
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function setOutputMode(outputName, modeId, callback) { function setOutputMode(outputName, modeId, callback) {
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": true, "enabled": true,
"modeId": modeId "modeId": modeId
} }]
];
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function setOutputCustomMode(outputName, width, height, refresh, callback) { function setOutputCustomMode(outputName, width, height, refresh, callback) {
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": true, "enabled": true,
"customMode": { "customMode": {
@@ -210,137 +216,60 @@ Singleton {
"height": height, "height": height,
"refresh": refresh "refresh": refresh
} }
} }]
];
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function setOutputPosition(outputName, x, y, callback) { function setOutputPosition(outputName, x, y, callback) {
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": true, "enabled": true,
"position": { "position": {
"x": x, "x": x,
"y": y "y": y
} }
} }]
];
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function setOutputScale(outputName, scale, callback) { function setOutputScale(outputName, scale, callback) {
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": true, "enabled": true,
"scale": scale "scale": scale
} }]
];
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function setOutputTransform(outputName, transform, callback) { function setOutputTransform(outputName, transform, callback) {
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": true, "enabled": true,
"transform": transform "transform": transform
} }]
];
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function setOutputAdaptiveSync(outputName, state, callback) { function setOutputAdaptiveSync(outputName, state, callback) {
const heads = [ const heads = [{
{
"name": outputName, "name": outputName,
"enabled": true, "enabled": true,
"adaptiveSync": state "adaptiveSync": state
} }]
];
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function configureOutput(config, callback) { function configureOutput(config, callback) {
const heads = [config]; const heads = [config]
applyConfiguration(heads, callback); applyConfiguration(heads, callback)
} }
function configureMultipleOutputs(configs, callback) { function configureMultipleOutputs(configs, callback) {
applyConfiguration(configs, callback); applyConfiguration(configs, callback)
}
// High-level apply matching the generateOutputsConfig() pattern used by
// NiriService, HyprlandService and DwlService. Instead of writing a
// config file, the changes are applied directly via the
// wlr-output-management protocol.
function applyOutputsConfig(outputsData, connectedOutputs) {
if (!wlrOutputAvailable)
return;
const heads = [];
for (const name in outputsData) {
if (!connectedOutputs[name])
continue;
const output = outputsData[name];
const mode = (output.modes && output.current_mode >= 0) ? output.modes[output.current_mode] : null;
const enabled = !!mode;
const head = {
"name": name,
"enabled": enabled
};
if (enabled) {
if (mode.id !== undefined)
head.modeId = mode.id;
else
head.customMode = {
"width": mode.width,
"height": mode.height,
"refresh": mode.refresh_rate
};
if (output.logical) {
head.position = {
"x": output.logical.x ?? 0,
"y": output.logical.y ?? 0
};
head.scale = output.logical.scale ?? 1.0;
head.transform = transformFromName(output.logical.transform);
}
}
heads.push(head);
}
if (heads.length > 0)
applyConfiguration(heads);
}
function transformFromName(name) {
switch (name) {
case "Normal":
return 0;
case "90":
return 1;
case "180":
return 2;
case "270":
return 3;
case "Flipped":
return 4;
case "Flipped90":
return 5;
case "Flipped180":
return 6;
case "Flipped270":
return 7;
default:
return 0;
}
} }
} }
-5
View File
@@ -121,12 +121,7 @@ Item {
dropdownMenu.close(); dropdownMenu.close();
return; return;
} }
dropdownMenu.open(); dropdownMenu.open();
let currentIndex = root.options.indexOf(root.currentValue);
listView.positionViewAtIndex(currentIndex, ListView.Beginning);
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0); const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
const popupW = dropdownMenu.width; const popupW = dropdownMenu.width;
const popupH = dropdownMenu.height; const popupH = dropdownMenu.height;
+209 -36
View File
@@ -20,10 +20,10 @@ Item {
property string triggerSection: "" property string triggerSection: ""
property string positioning: "center" property string positioning: "center"
property int animationDuration: Theme.popoutAnimationDuration property int animationDuration: Theme.popoutAnimationDuration
property real animationScaleCollapsed: 0.96 property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.spacingL property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationExitCurve: Theme.variantPopoutExitCurve
property bool suspendShadowWhileResizing: false property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false property bool shouldBeVisible: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
@@ -74,6 +74,7 @@ Item {
signal backgroundClicked signal backgroundClicked
property var _lastOpenedScreen: null property var _lastOpenedScreen: null
property bool isClosing: false
property int effectiveBarPosition: 0 property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0 property real effectiveBarBottomGap: 0
@@ -156,10 +157,14 @@ Item {
} }
} }
property bool animationsEnabled: true
function open() { function open() {
if (!screen) if (!screen)
return; return;
closeTimer.stop(); closeTimer.stop();
isClosing = false;
animationsEnabled = false;
// Snapshot mask geometry // Snapshot mask geometry
_frozenMaskX = maskX; _frozenMaskX = maskX;
@@ -174,12 +179,22 @@ Item {
} }
_lastOpenedScreen = screen; _lastOpenedScreen = screen;
shouldBeVisible = true; if (contentContainer) {
contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr);
contentContainer.scaleValue = root.animationScaleCollapsed;
}
if (useBackgroundWindow) { if (useBackgroundWindow) {
_surfaceMarginLeft = alignedX - shadowBuffer; _surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2; _surfaceW = alignedWidth + shadowBuffer * 2;
backgroundWindow.visible = true;
} }
contentWindow.visible = true;
Qt.callLater(() => { Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (shouldBeVisible && screen) { if (shouldBeVisible && screen) {
if (useBackgroundWindow) if (useBackgroundWindow)
backgroundWindow.visible = true; backgroundWindow.visible = true;
@@ -191,6 +206,7 @@ Item {
} }
function close() { function close() {
isClosing = true;
shouldBeVisible = false; shouldBeVisible = false;
_primeContent = false; _primeContent = false;
PopoutManager.popoutChanged(); PopoutManager.popoutChanged();
@@ -222,9 +238,10 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration interval: Theme.variantCloseInterval(animationDuration)
onTriggered: { onTriggered: {
if (!shouldBeVisible) { if (!shouldBeVisible) {
isClosing = false;
contentWindow.visible = false; contentWindow.visible = false;
if (useBackgroundWindow) if (useBackgroundWindow)
backgroundWindow.visible = false; backgroundWindow.visible = false;
@@ -241,7 +258,16 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: Math.max(0, animationOffset) readonly property real shadowMotionPadding: {
if (Theme.isDirectionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0)
return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping.
return Math.max(0, animationOffset) + 16;
}
if (Theme.isDepthEffect)
return Math.max(0, animationOffset) + 8;
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, dpr) readonly property real alignedHeight: Theme.px(popupHeight, dpr)
@@ -353,6 +379,10 @@ Item {
mask: Region { mask: Region {
item: maskRect item: maskRect
Region {
item: contentExclusionRect
intersection: Intersection.Subtract
}
} }
Rectangle { Rectangle {
@@ -361,26 +391,70 @@ Item {
color: "transparent" color: "transparent"
x: root._frozenMaskX x: root._frozenMaskX
y: root._frozenMaskY y: root._frozenMaskY
width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0 width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0
height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0 height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0
} }
MouseArea { Item {
id: contentExclusionRect
visible: false
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
Item {
id: outsideClickCatcher
x: root._frozenMaskX x: root._frozenMaskX
y: root._frozenMaskY y: root._frozenMaskY
width: root._frozenMaskWidth width: root._frozenMaskWidth
height: root._frozenMaskHeight height: root._frozenMaskHeight
hoverEnabled: false enabled: root.shouldBeVisible && root.backgroundInteractive
enabled: shouldBeVisible && backgroundInteractive
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + root._frozenMaskX;
const clickY = mouse.y + root._frozenMaskY;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
if (!outsideContent) readonly property real contentLeft: Math.max(0, root.alignedX - x)
return; readonly property real contentTop: Math.max(0, root.alignedY - y)
backgroundClicked(); readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth)
readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight)
MouseArea {
x: 0
y: 0
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentBottom
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.contentLeft)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: outsideClickCatcher.contentRight
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
} }
} }
@@ -425,7 +499,6 @@ Item {
} }
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
anchors { anchors {
left: true left: true
top: true top: true
@@ -483,12 +556,70 @@ Item {
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0) readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0) readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
readonly property real offsetX: {
if (directionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
return 0;
if (barLeft)
return -directionalTravelX;
if (barRight)
return directionalTravelX;
if (barTop || barBottom)
return 0;
return sectionTilt * directionalTravelX * 0.2;
}
if (depthEffect) {
if (barLeft)
return -depthTravel;
if (barRight)
return depthTravel;
if (barTop || barBottom)
return 0;
return sectionTilt * depthTravel * 0.2;
}
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
}
readonly property real offsetY: {
if (directionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
return 0;
if (barBottom)
return directionalTravelY;
if (barTop)
return -directionalTravelY;
if (barLeft || barRight)
return 0;
return directionalTravelY;
}
if (depthEffect) {
if (barBottom)
return depthTravel;
if (barTop)
return -depthTravel;
if (barLeft || barRight)
return 0;
return depthTravel;
}
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
}
property real animX: 0 property real animX: 0
property real animY: 0 property real animY: 0
property real scaleValue: root.animationScaleCollapsed
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: computedScaleCollapsed
Component.onCompleted: {
animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr);
animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr);
scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed;
}
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
@@ -498,34 +629,72 @@ Item {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr); contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr); contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed; contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : contentContainer.computedScaleCollapsed;
} }
} }
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Item {
id: directionalClipMask
readonly property bool shouldClip: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect
readonly property real clipOversize: 1000
clip: shouldClip
// Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows
x: shouldClip ? (contentContainer.barRight ? -clipOversize : (contentContainer.barLeft ? 0 : -clipOversize)) : 0
y: shouldClip ? (contentContainer.barBottom ? -clipOversize : (contentContainer.barTop ? 0 : -clipOversize)) : 0
width: shouldClip ? parent.width + clipOversize + (contentContainer.barLeft || contentContainer.barRight ? 0 : clipOversize) : parent.width
height: shouldClip ? parent.height + clipOversize + (contentContainer.barTop || contentContainer.barBottom ? 0 : clipOversize) : parent.height
Item {
id: aligner
readonly property real baseWidth: contentContainer.width
readonly property real baseHeight: contentContainer.height
readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect
x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0)
y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0)
width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth
height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight
clip: isRollOut
Item {
id: unrollCounteract
x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
width: aligner.baseWidth
height: aligner.baseHeight
ElevationShadow { ElevationShadow {
id: shadowSource id: shadowSource
width: parent.width width: parent.width
@@ -544,22 +713,23 @@ Item {
Item { Item {
id: contentWrapper id: contentWrapper
anchors.centerIn: parent
width: parent.width width: parent.width
height: parent.height height: parent.height
opacity: shouldBeVisible ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
visible: opacity > 0 visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr)
layer.enabled: contentWrapper.opacity < 1 layer.enabled: contentWrapper.opacity < 1
layer.smooth: false layer.smooth: false
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: animationDuration duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
@@ -579,8 +749,11 @@ Item {
active: root._primeContent || shouldBeVisible || contentWindow.visible active: root._primeContent || shouldBeVisible || contentWindow.visible
asynchronous: false asynchronous: false
} }
} } // closes contentWrapper
} } // closes unrollCounteract
} // closes aligner
} // closes directionalClipMask
} // closes contentContainer
Item { Item {
id: focusHelper id: focusHelper
+2 -14
View File
@@ -239,7 +239,7 @@ Item {
StyledRect { StyledRect {
id: valueTooltip id: valueTooltip
width: tooltipText.reservedWidth + Theme.spacingS * 2 width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2 height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainer color: Theme.surfaceContainer
@@ -251,22 +251,10 @@ Item {
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue)) visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
opacity: visible ? 1 : 0 opacity: visible ? 1 : 0
NumericText { StyledText {
id: tooltipText id: tooltipText
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
reserveText: {
let widest = "";
const samples = [slider.minimum, slider.maximum];
if (slider.valueOverride >= 0)
samples.push(slider.valueOverride);
for (let i = 0; i < samples.length; i++) {
const candidate = Math.round(samples[i]) + slider.unit;
if (candidate.length > widest.length)
widest = candidate;
}
return widest;
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
+12 -17
View File
@@ -99,12 +99,10 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
// M3 disabled track: on surface 12% opacity color: (checked && enabled) ? Theme.primary : Theme.surfaceVariantAlpha
color: !toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha) opacity: toggling ? 0.6 : (enabled ? 1 : 0.4)
opacity: toggle.toggling ? 0.6 : 1
// M3 disabled unchecked border: on surface 12% opacity border.color: (!checked || !enabled) ? Theme.outline : "transparent"
border.color: toggle.checked ? "transparent" : (!toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.outline)
readonly property int pad: Math.round((height - thumb.width) / 2) readonly property int pad: Math.round((height - thumb.width) / 2)
readonly property int edgeLeft: pad readonly property int edgeLeft: pad
@@ -113,18 +111,16 @@ Item {
StyledRect { StyledRect {
id: thumb id: thumb
width: toggle.checked ? insetCircle : insetCircle - 4 width: (checked && enabled) ? insetCircle : insetCircle - 4
height: toggle.checked ? insetCircle : insetCircle - 4 height: (checked && enabled) ? insetCircle : insetCircle - 4
radius: Theme.cornerRadius radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// M3 disabled thumb: color: (checked && enabled) ? Theme.surface : Theme.outline
// checked = solid surface | unchecked = on surface 38% border.color: (checked && enabled) ? Theme.outline : Theme.outline
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Qt.alpha(Theme.surfaceText, 0.38)) : (toggle.checked ? Theme.surface : Theme.outline) border.width: (checked && enabled) ? 1 : 2
border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
border.width: (toggle.checked && toggle.enabled) ? 1 : 2
x: toggle.checked ? toggleTrack.edgeRight : toggleTrack.edgeLeft x: (checked && enabled) ? toggleTrack.edgeRight : toggleTrack.edgeLeft
Behavior on x { Behavior on x {
SequentialAnimation { SequentialAnimation {
@@ -162,11 +158,10 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
name: "check" name: "check"
size: 20 size: 20
// M3 disabled icon: on surface 38% color: Theme.surfaceText
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
filled: true filled: true
opacity: toggle.checked ? 1 : 0 opacity: checked && enabled ? 1 : 0
scale: toggle.checked ? 1 : 0.6 scale: checked && enabled ? 1 : 0.6
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
-22
View File
@@ -1,22 +0,0 @@
import QtQuick
import qs.Common
StyledText {
id: root
property string reserveText: ""
readonly property real reservedWidth: reserveText !== "" ? Math.max(contentWidth, reserveMetrics.width) : contentWidth
isMonospace: true
wrapMode: Text.NoWrap
StyledTextMetrics {
id: reserveMetrics
isMonospace: root.isMonospace
font.pixelSize: root.font.pixelSize
font.family: root.font.family
font.weight: root.font.weight
font.hintingPreference: root.font.hintingPreference
text: root.reserveText
}
}
+5 -10
View File
@@ -75,6 +75,11 @@ Rectangle {
"label": I18n.tr("Auth Type"), "label": I18n.tr("Auth Type"),
"value": data["connection-type"] "value": data["connection-type"]
}); });
fields.push({
"key": "auto",
"label": I18n.tr("Autoconnect"),
"value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
return fields; return fields;
} }
@@ -266,16 +271,6 @@ Rectangle {
} }
} }
DankToggle {
width: parent.width
text: I18n.tr("Autoconnect")
checked: configData ? (configData.autoconnect || false) : false
visible: !VPNService.configLoading && configData !== null
onToggled: checked => {
VPNService.updateConfig(profile.uuid, {autoconnect: checked});
}
}
Item { Item {
width: 1 width: 1
height: Theme.spacingXS height: Theme.spacingXS

Some files were not shown because too many files have changed in this diff Show More