1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 18:42:06 -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
144 changed files with 4545 additions and 25983 deletions

View File

@@ -20,7 +20,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install 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
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install 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
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: core/go.mod

View File

@@ -32,13 +32,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
@@ -106,7 +106,7 @@ jobs:
- name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: core-assets-${{ matrix.arch }}
path: |
@@ -120,7 +120,7 @@ jobs:
- name: Upload artifacts with completions
if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: core-assets-${{ matrix.arch }}
path: |
@@ -147,7 +147,7 @@ jobs:
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
# - name: Checkout
# uses: actions/checkout@v6
# uses: actions/checkout@v4
# with:
# token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0
@@ -181,7 +181,7 @@ jobs:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
@@ -192,12 +192,12 @@ jobs:
git checkout ${TAG}
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Download core artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
pattern: core-assets-*
merge-multiple: true

View File

@@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Determine version
id: version
@@ -134,7 +134,7 @@ jobs:
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -195,13 +195,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Wait before OBS upload
run: sleep 3
- name: Determine packages to update
id: packages
run: |
@@ -347,7 +344,7 @@ jobs:
done
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod

View File

@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -157,12 +157,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
cache: false

View File

@@ -24,7 +24,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
@@ -40,7 +40,7 @@ jobs:
echo "Build succeeded, no hash update needed"
exit 0
fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else
echo "No changes to flake.nix"
fi

View File

@@ -86,9 +86,7 @@ touch .qmlls.ini
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`.
6. Make your changes, test, and open a pull request.
5. Make your changes, test, and open a pull request.
### I18n/Localization

View File

@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
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
@@ -32,9 +32,6 @@ clean:
@$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete"
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Installation targets
install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -79,7 +76,7 @@ install-desktop:
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed"
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
@echo ""
@echo "Installation complete!"
@echo ""
@@ -133,7 +130,6 @@ help:
@echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'"
@echo " clean - Clean build artifacts"
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
@echo ""
@echo "Install:"
@echo " install - Build and install everything (requires sudo)"

View File

@@ -1,26 +1,13 @@
repos:
- repo: local
- repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
hooks:
- id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-config-verify
name: golangci-lint-config-verify
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...

View File

@@ -63,19 +63,19 @@ endif
build-all: build dankinstall
install:
install: build
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installation complete"
install-all:
install-all: build-all
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete"
install-dankinstall:
install-dankinstall: dankinstall
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete"

View File

@@ -1,10 +0,0 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}

View File

@@ -222,19 +222,16 @@ func init() {
func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte
copyFromStdin := false
switch {
case len(args) > 0:
data = []byte(args[0])
case clipCopyDownload || clipCopyType == "__multi__":
default:
var err error
data, err = io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("read stdin: %v", err)
}
default:
copyFromStdin = true
}
if clipCopyDownload {
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
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 {
log.Fatalf("copy: %v", err)
}

View File

@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
if currentCategory != -1 {
sb.WriteString("\n")
}
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
currentCategory = r.category
}
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
if doctorVerbose && r.details != "" {
fmt.Fprintf(&sb, " - %s\n", r.details)
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
}
}
@@ -1096,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
}
sb.WriteString("\n---\n")
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
return sb.String()
}

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@ func init() {
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {

View File

@@ -16,10 +16,9 @@ import (
)
var setupCmd = &cobra.Command{
Use: "setup",
Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand,
Use: "setup",
Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err)

View File

@@ -1,271 +0,0 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}

View File

@@ -16,10 +16,19 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
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)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd)

View File

@@ -11,20 +11,29 @@ import (
var Version = "dev"
func init() {
// Add flags
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("session", false, "Session managed (like as a systemd unit)")
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)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate())
}
func main() {
// Block root
if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.")
}

View File

@@ -7,6 +7,14 @@ import (
"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 {
cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run()

View File

@@ -1,8 +1,6 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.0
toolchain go1.26.1
go 1.25.0
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0

View File

@@ -1,12 +1,10 @@
package clipboard
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
@@ -14,37 +12,17 @@ import (
)
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 {
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 {
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"}
if pasteOnce {
args = append(args, "--paste-once")
@@ -52,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe()
if err != nil {
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)
}
if _, err := io.Copy(stdin, data); err != nil {
if _, err := stdin.Write(data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
stdin.Close()
return nil
}
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
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{})
pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if err := writeTo(file); err != nil {
select {
case sendErr <- err:
default:
}
}
file.Write(data)
select {
case pasted <- struct{}{}:
default:
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
select {
case <-cancelled:
return nil
case err := <-sendErr:
return err
case <-pasted:
if pasteOnce {
return nil

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, ", ")))
hasNiri := false
hasQuickshell := false
for _, pkg := range packages {
if pkg == "niri-git" {
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
@@ -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)
}
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
if pkg == "dms-shell-bin" {
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),
}
} else {
// Skip dependency installation for dms-shell-git and dms-shell-bin
// since we manually manage those dependencies
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
// Pre-install dependencies from .SRCINFO
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
@@ -615,19 +628,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
CommandInfo: "Installing package dependencies and makedepends",
}
// Install dependencies from .SRCINFO
depFilter := ""
if pkg == "dms-shell-git" {
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
}
// Install dependencies and makedepends explicitly
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %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
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
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 {
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 {
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{
@@ -656,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
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 {
return fmt.Errorf("failed to build %s: %w", pkg, err)

View File

@@ -92,25 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
}
func (d *DebianDistribution) packageInstalled(pkg string) bool {
return debianPackageInstalledPrecisely(pkg)
}
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
cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
return err == nil
}
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...",
IsComplete: false,
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",
}
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 {
return fmt.Errorf("failed to install development tools: %w", err)
}
@@ -395,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
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 {
enabledRepos := make(map[string]bool)
@@ -506,46 +482,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
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{
Phase: PhaseSystemPackages,
Progress: startProgress,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {

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, ", ")))
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 {
@@ -494,57 +515,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, 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"}
if minimal {
args = append(args, "--setopt=install_weak_deps=False")
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
return append(args, packages...)
}
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)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing COPR 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, PhaseAURPackages, 0.70, 0.85)
}

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
}

View File

@@ -29,8 +29,6 @@ type OpenSUSEDistribution struct {
config DistroConfig
}
const openSUSENiriWaylandServerPackage = "libwayland-server0"
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{
@@ -201,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
}
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 {
@@ -271,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
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 {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
@@ -305,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
NeedsSudo: true,
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)
}
}
@@ -320,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
IsComplete: false,
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)
}
}
@@ -410,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
}
}
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
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 {
names := make([]string, len(packages))
for i, pkg := range packages {
@@ -515,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
return nil
}
func isOpenSUSEInstallMediaURI(uri string) bool {
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
return strings.HasPrefix(normalizedURI, "cd:/") ||
strings.HasPrefix(normalizedURI, "dvd:/") ||
strings.HasPrefix(normalizedURI, "hd:/") ||
strings.HasPrefix(normalizedURI, "iso:/")
}
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 7 {
continue
}
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
alias := parts[1]
enabled := strings.ToLower(parts[3])
uri := parts[len(parts)-1]
if alias == "" || strings.EqualFold(alias, "alias") {
continue
}
if enabled != "" && enabled != "yes" {
continue
}
if !isOpenSUSEInstallMediaURI(uri) {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
output, err := listCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
return fmt.Errorf("failed to list zypper repositories: %w", err)
}
aliases := parseZypperInstallMediaAliases(string(output))
if len(aliases) == 0 {
return nil
}
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.055,
Step: "Disabling install media repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
}
for _, alias := range aliases {
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
}
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
}
return nil
}
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
args := []string{"zypper", "install", "-y"}
if minimal {
args = append(args, "--no-recommends")
}
return append(args, packages...)
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
args := []string{"zypper", "install", "-y"}
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{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {

View File

@@ -100,7 +100,9 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
}
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 {
@@ -452,7 +454,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, 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 {
@@ -461,59 +477,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, 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{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
args := []string{"apt-get", "install", "-y"}
args = 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{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing PPA packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
}
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {

View File

@@ -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

View File

@@ -71,7 +71,6 @@ var templateRegistry = []TemplateDef{
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
}
func (c *ColorMode) GTKTheme() string {

View File

@@ -6,20 +6,12 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/pilebones/go-udev/netlink"
)
const (
udevRecvBufSize = 8 * 1024 * 1024
udevMaxRetries = 5
udevBaseDelay = 2 * time.Second
udevMaxDelay = 60 * time.Second
)
type UdevMonitor struct {
stop chan struct{}
rescanMutex sync.Mutex
@@ -37,6 +29,13 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
}
func (m *UdevMonitor) run(manager *Manager) {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
log.Errorf("Failed to connect to udev netlink: %v", err)
return
}
defer conn.Close()
matcher := &netlink.RuleDefinitions{
Rules: []netlink.RuleDefinition{
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
@@ -49,46 +48,6 @@ func (m *UdevMonitor) run(manager *Manager) {
return
}
failures := 0
for {
if err := m.monitorLoop(manager, matcher); err != nil {
log.Errorf("Udev monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > udevMaxRetries {
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
return
}
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
return err
}
defer conn.Close()
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
log.Warnf("Failed to set udev socket receive buffer: %v", err)
}
events := make(chan netlink.UEvent)
errs := make(chan error)
conn.Monitor(events, errs, matcher)
@@ -98,9 +57,10 @@ func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinit
for {
select {
case <-m.stop:
return nil
return
case err := <-errs:
return err
log.Errorf("Udev monitor error: %v", err)
return
case event := <-events:
m.handleEvent(manager, event)
}

View File

@@ -1,7 +1,6 @@
package network
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) {
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)
if assert.NotNil(t, result) {
assert.NotEmpty(t, result.ChosenReason)
}
assert.NotNil(t, result)
assert.NotEmpty(t, result.ChosenReason)
}

View File

@@ -73,7 +73,6 @@ var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
var locationManager *location.Manager
var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client"
@@ -192,7 +191,7 @@ func InitializeFreedeskManager() error {
return nil
}
func InitializeWaylandManager() error {
func InitializeWaylandManager(geoClient geolocation.Client) error {
log.Info("Attempting to initialize Wayland gamma control...")
if wlContext == nil {
@@ -205,7 +204,7 @@ func InitializeWaylandManager() error {
}
config := wayland.DefaultConfig()
manager, err := wayland.NewManager(wlContext.Display(), config)
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
if err != nil {
log.Errorf("Failed to initialize wayland manager: %v", err)
return err
@@ -386,8 +385,8 @@ func InitializeDbusManager() error {
return nil
}
func InitializeThemeModeManager() error {
manager := thememode.NewManager()
func InitializeThemeModeManager(geoClient geolocation.Client) error {
manager := thememode.NewManager(geoClient)
themeModeManager = manager
log.Info("Theme mode automation manager initialized")
@@ -1331,9 +1330,6 @@ func cleanupManagers() {
if locationManager != nil {
locationManager.Close()
}
if geoClientInstance != nil {
geoClientInstance.Close()
}
}
func Start(printDocs bool) error {
@@ -1549,6 +1545,9 @@ func Start(printDocs bool) error {
loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{})
geoClient := geolocation.NewClient()
defer geoClient.Close()
go func() {
defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil {
@@ -1593,41 +1592,10 @@ func Start(printDocs bool) error {
}
}()
if err := InitializeWaylandManager(); err != nil {
if err := InitializeWaylandManager(geoClient); err != nil {
log.Warnf("Wayland manager unavailable: %v", err)
}
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
go func() {
geoClient := geolocation.NewClient()
geoClientInstance = geoClient
if waylandManager != nil {
waylandManager.SetGeoClient(geoClient)
}
if themeModeManager != nil {
themeModeManager.SetGeoClient(geoClient)
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
}()
go func() {
if err := InitializeBluezManager(); err != nil {
log.Warnf("Bluez manager unavailable: %v", err)
@@ -1656,6 +1624,25 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err)
}
if err := InitializeThemeModeManager(geoClient); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil {
go func() {

View File

@@ -40,7 +40,7 @@ type Manager struct {
wg sync.WaitGroup
}
func NewManager() *Manager {
func NewManager(geoClient geolocation.Client) *Manager {
m := &Manager{
config: Config{
Enabled: false,
@@ -54,6 +54,7 @@ func NewManager() *Manager {
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
geoClient: geoClient,
}
m.updateState(time.Now())
@@ -314,10 +315,6 @@ func (m *Manager) getConfig() Config {
return m.config
}
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude
@@ -325,9 +322,6 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
if !config.UseIPLocation {
return nil, nil
}
if m.geoClient == nil {
return nil, nil
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {

View File

@@ -20,7 +20,7 @@ import (
const animKelvinStep = 25
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
if err := config.Validate(); err != nil {
return nil, err
}
@@ -41,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
updateTrigger: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16),
geoClient: geoClient,
}
if err := m.setupRegistry(); err != nil {
@@ -421,10 +422,6 @@ func (m *Manager) recalcSchedule(now time.Time) {
}
}
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation() (*float64, *float64) {
m.configMutex.RLock()
config := m.config
@@ -433,31 +430,27 @@ func (m *Manager) getLocation() (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude
}
if !config.UseIPLocation {
return nil, nil
}
if m.geoClient == nil {
return nil, nil
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
if config.UseIPLocation {
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
}
return nil, nil
}
func (m *Manager) hasValidSchedule() bool {

View File

@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
)
@@ -390,18 +391,20 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
config := DefaultConfig()
_, err := NewManager(mockDisplay, config)
_, err := NewManager(mockDisplay, mockGeoclient, config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "get registry")
}
func TestNewManager_InvalidConfig(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
config := Config{
LowTemp: 500,
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
Gamma: 1.0,
}
_, err := NewManager(mockDisplay, config)
_, err := NewManager(mockDisplay, mockGeoclient, config)
assert.Error(t, err)
}

View File

@@ -2,10 +2,10 @@ package wlcontext
import (
"fmt"
"golang.org/x/sys/unix"
"os"
"sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -123,9 +123,6 @@ func (sc *SharedContext) eventDispatcher() {
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
}
consecutiveErrors := 0
const maxConsecutiveErrors = 20
for {
sc.drainCmdQueue()
@@ -156,19 +153,9 @@ func (sc *SharedContext) eventDispatcher() {
}
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
consecutiveErrors++
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
return
}
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
continue
log.Errorf("Wayland connection error: %v", err)
return
}
consecutiveErrors = 0
}
}

View File

@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
spinner := m.spinner.View()
status := m.styles.Normal.Render("Setting up configuration files...")
fmt.Fprintf(&b, "%s %s", spinner, status)
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
b.WriteString("\n\n")
// Show progress information

View File

@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
spinner := m.spinner.View()
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
fmt.Fprintf(&b, "%s %s", spinner, status)
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
return b.String()
}

View File

@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
if !m.packageProgress.isComplete {
spinner := m.spinner.View()
status := m.styles.Normal.Render(m.packageProgress.step)
fmt.Fprintf(&b, "%s %s", spinner, status)
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
b.WriteString("\n\n")
// Show progress bar
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
for i := startIdx; i < len(allLogs); i++ {
if allLogs[i] != "" {
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
}
}

View File

@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
spinner := m.spinner.View()
status := m.styles.Normal.Render("Waiting for fingerprint...")
fmt.Fprintf(&b, "%s %s", spinner, status)
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
}
return b.String()

View File

@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
contentStyle = contentStyle.Bold(true)
}
fmt.Fprintf(&b, " %s %s\n",
b.WriteString(fmt.Sprintf(" %s %s\n",
prefixStyle.Render(prefix),
contentStyle.Render(content))
contentStyle.Render(content)))
}
b.WriteString("\n")
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
} else if m.isLoading {
spinner := m.spinner.View()
loading := m.styles.Normal.Render("Detecting system...")
fmt.Fprintf(&b, "%s %s\n\n", spinner, loading)
b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading))
}
// Footer with better visual separation

View File

@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Suggests: niri | hyprland | sway
Recommends: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.

View File

@@ -24,7 +24,6 @@ let
lib.makeBinPath [
cfg.quickshell.package
compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
]
}
${
@@ -196,9 +195,7 @@ in
fi
if [ -f settings.json ]; then
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
cp "$theme_file" custom-theme.json
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi

View File

@@ -2,9 +2,11 @@
config,
lib,
...
}: let
}:
let
cfg = config.programs.dank-material-shell;
in {
in
{
imports = [
./dms-rename.nix
];
@@ -14,11 +16,9 @@ in {
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
includes = {
enable =
(lib.mkEnableOption "includes for niri-flake")
// {
default = true;
};
enable = (lib.mkEnableOption "includes for niri-flake") // {
default = true;
};
override = lib.mkOption {
type = lib.types.bool;
description = ''
@@ -44,10 +44,8 @@ in {
"alttab"
"binds"
"colors"
"cursor"
"layout"
"outputs"
"windowrules"
"wpblur"
];
example = [
@@ -72,21 +70,24 @@ in {
let
cfg' = cfg.niri.includes;
withOriginalConfig = dmsFiles:
if cfg'.override
then [cfg'.originalFileName] ++ dmsFiles
else dmsFiles ++ [cfg'.originalFileName];
withOriginalConfig =
dmsFiles:
if cfg'.override then
[ cfg'.originalFileName ] ++ dmsFiles
else
dmsFiles ++ [ cfg'.originalFileName ];
fixes = map (fix: "\n${fix}") (
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
# kdl
''
// Border fix
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
layout { border { on; }; }
''
# kdl
''
// Border fix
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
layout { border { on; }; }
''
);
in {
in
{
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
niri-config-dms = {
target = "niri/config.kdl";
@@ -103,9 +104,11 @@ in {
programs.niri.settings = lib.mkMerge [
(lib.mkIf cfg.niri.enableKeybinds {
binds = with config.lib.niri.actions; let
dms-ipc = spawn "dms" "ipc";
in
binds =
with config.lib.niri.actions;
let
dms-ipc = spawn "dms" "ipc";
in
{
"Mod+Space" = {
action = dms-ipc "spotlight" "toggle";

View File

@@ -102,19 +102,6 @@ if [[ ! -d "distro/debian" ]]; then
echo "Error: Run this script from the repository root"
exit 1
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:
# $1 = PROJECT
# $2 = PACKAGE
@@ -322,23 +309,8 @@ mkdir -p "$OBS_BASE"
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
echo "Checking out $OBS_PROJECT/$PACKAGE..."
cd "$OBS_BASE"
CHECKOUT_OK=false
for attempt in 1 2 3; do
if osc co "$OBS_PROJECT/$PACKAGE"; then
CHECKOUT_OK=true
break
fi
if [[ $attempt -lt 3 ]]; then
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
sleep $((5*attempt))
fi
done
osc co "$OBS_PROJECT/$PACKAGE"
cd "$REPO_ROOT"
if [[ "$CHECKOUT_OK" != "true" ]]; then
echo "Error: Checkout failed after 3 attempts"
exit 1
fi
fi
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)
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"
exit 1
fi
@@ -1173,7 +1145,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
else
echo "==> Committing to OBS"
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]}
set -e
if [[ $COMMIT_EXIT -ne 0 ]]; then

View File

@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Suggests: niri | hyprland | sway
Recommends: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.

139
flake.nix
View File

@@ -17,25 +17,6 @@
...
}:
let
goModVersion =
let
content = builtins.readFile ./core/go.mod;
lines = builtins.filter builtins.isString (builtins.split "\n" content);
goLines = builtins.filter (l: builtins.match "go [0-9]+\\..*" l != null) lines;
matched =
if goLines != [ ] then builtins.match "go ([0-9]+)\\.([0-9]+).*" (builtins.head goLines) else null;
in
if matched != null then
{
major = builtins.elemAt matched 0;
minor = builtins.elemAt matched 1;
}
else
{
major = "1";
minor = "25";
};
goForPkgs = pkgs: pkgs.${"go_${goModVersion.major}_${goModVersion.minor}"};
forEachSystem =
fn:
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
@@ -91,82 +72,76 @@
"${cleanVersion}${dateSuffix}${revSuffix}";
in
{
dms-shell = pkgs.lib.makeOverridable (
dms-shell = pkgs.buildGoModule (
let
rootSrc = ./.;
in
{
extraQtPackages ? [ ],
}:
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
subPackages = [ "cmd/dms" ];
subPackages = [ "cmd/dms" ];
ldflags = [
"-s"
"-w"
"-X 'main.Version=${version}'"
];
ldflags = [
"-s"
"-w"
"-X 'main.Version=${version}'"
];
nativeBuildInputs = with pkgs; [
installShellFiles
makeWrapper
];
nativeBuildInputs = with pkgs; [
installShellFiles
makeWrapper
];
postInstall = ''
mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
postInstall = ''
mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION
chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg
# Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh)
'';
installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh)
'';
meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit;
mainProgram = "dms";
platforms = pkgs.lib.platforms.linux;
};
}
)
) { };
meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit;
mainProgram = "dms";
platforms = pkgs.lib.platforms.linux;
};
}
);
quickshell = quickshell.packages.${system}.default;
@@ -206,7 +181,7 @@
buildInputs =
with pkgs;
[
(goForPkgs pkgs)
go_1_25
go-mockery_2
gopls
delve

View File

@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
# Code formatting and linting
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
```
@@ -783,7 +783,7 @@ When modifying the shell:
**QML Frontend:**
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)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency

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;
}
}
}

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 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]
// 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]
}

View File

@@ -22,8 +22,8 @@ Singleton {
property bool _hasUnsavedChanges: false
property var _loadedSessionSnapshot: null
readonly property var _hooks: ({
"updateLocale": updateLocale
})
"updateLocale": updateLocale
})
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -580,7 +580,14 @@ Singleton {
}
}
newSettings[identifier] = getMonitorCyclingSettings(screenName);
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].enabled = enabled;
monitorCyclingSettings = newSettings;
saveSettings();
@@ -611,7 +618,14 @@ Singleton {
}
}
newSettings[identifier] = getMonitorCyclingSettings(screenName);
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].mode = mode;
monitorCyclingSettings = newSettings;
saveSettings();
@@ -642,7 +656,14 @@ Singleton {
}
}
newSettings[identifier] = getMonitorCyclingSettings(screenName);
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].interval = interval;
monitorCyclingSettings = newSettings;
saveSettings();
@@ -673,7 +694,14 @@ Singleton {
}
}
newSettings[identifier] = getMonitorCyclingSettings(screenName);
if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].time = time;
monitorCyclingSettings = newSettings;
saveSettings();
@@ -1190,7 +1218,7 @@ Singleton {
"time": "06:00"
};
var value = _findMonitorValue(monitorCyclingSettings, screenName);
return Object.assign({}, defaults, value !== undefined ? value : {});
return value !== undefined ? value : defaults;
}
FileView {
@@ -1217,7 +1245,7 @@ Singleton {
id: greeterSessionFile
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";
}
preload: isGreeterMode

View File

@@ -37,6 +37,18 @@ Singleton {
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 {
Suspend,
Hibernate,
@@ -166,6 +178,12 @@ Singleton {
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
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
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12
@@ -280,7 +298,6 @@ Singleton {
property bool showOccupiedWorkspacesOnly: false
property bool reverseScrolling: false
property bool dwlShowAllTags: false
property bool workspaceActiveAppHighlightEnabled: false
property string workspaceColorMode: "default"
property string workspaceOccupiedColorMode: "none"
property string workspaceUnfocusedColorMode: "default"
@@ -314,17 +331,6 @@ Singleton {
property string centeringMode: "index"
property string clockDateFormat: ""
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 string appLauncherViewMode: "list"
@@ -468,25 +474,18 @@ Singleton {
property bool matugenTemplateGhostty: true
property bool matugenTemplateKitty: true
property bool matugenTemplateFoot: true
property bool matugenTemplateNeovim: false
property bool matugenTemplateNeovim: true
property bool matugenTemplateAlacritty: true
property bool matugenTemplateWezterm: true
property bool matugenTemplateDgop: true
property bool matugenTemplateKcolorscheme: true
property bool matugenTemplateVscode: true
property bool matugenTemplateEmacs: true
property bool matugenTemplateZed: true
property var matugenTemplateNeovimSettings: ({
"dark": { "baseTheme": "github_dark", "harmony": 0.5 },
"light": { "baseTheme": "github_light", "harmony": 0.5 }
})
property bool showDock: false
property bool dockAutoHide: false
property bool dockSmartAutoHide: false
property bool dockGroupByApp: false
property bool dockRestoreSpecialWorkspaceOnClick: false
property bool dockOpenOnOverview: false
property int dockPosition: SettingsData.Position.Bottom
property real dockSpacing: 4
@@ -532,23 +531,9 @@ Singleton {
property bool enableFprint: false
property int maxFprintTries: 15
property bool fprintdAvailable: false
property bool lockFingerprintCanEnable: false
property bool lockFingerprintReady: false
property string lockFingerprintReason: "probe_failed"
property bool greeterFingerprintCanEnable: false
property bool greeterFingerprintReady: false
property string greeterFingerprintReason: "probe_failed"
property string greeterFingerprintSource: "none"
property bool enableU2f: false
property string u2fMode: "or"
property bool u2fAvailable: false
property bool lockU2fCanEnable: false
property bool lockU2fReady: false
property string lockU2fReason: "probe_failed"
property bool greeterU2fCanEnable: false
property bool greeterU2fReady: false
property string greeterU2fReason: "probe_failed"
property string greeterU2fSource: "none"
property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0
@@ -571,7 +556,6 @@ Singleton {
property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true
property var notificationRules: []
property bool notificationFocusedMonitor: false
property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter
@@ -1035,19 +1019,13 @@ Singleton {
signal widgetDataChanged
signal workspaceIconsUpdated
function refreshAuthAvailability() {
if (isGreeterMode)
return;
Processes.settingsRoot = root;
Processes.detectAuthCapabilities();
}
Component.onCompleted: {
if (!isGreeterMode) {
Processes.settingsRoot = root;
loadSettings();
initializeListModels();
refreshAuthAvailability();
Processes.detectFprintd();
Processes.detectU2f();
Processes.checkPluginSettings();
}
}
@@ -1195,7 +1173,7 @@ Singleton {
"updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor
"updateCompositorCursor": updateCompositorCursor,
})
function set(key, value) {
@@ -1287,47 +1265,10 @@ Singleton {
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() {
try {
parsePluginSettings(pluginSettingsFile.text());
} catch (e) {
const msg = e.message || String(e);
if (!_isMissingPluginSettingsError(e))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
}
_pluginSettingsLoading = true;
parsePluginSettings(pluginSettingsFile.text());
_pluginSettingsLoading = false;
}
function parsePluginSettings(content) {
@@ -2763,7 +2704,6 @@ Singleton {
blockLoading: true
blockWrites: true
atomicWrites: true
printErrors: false
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
@@ -2772,10 +2712,7 @@ Singleton {
}
onLoadFailed: error => {
if (!isGreeterMode) {
const msg = String(error || "");
if (!_isMissingPluginSettingsError(error))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
pluginSettings = {};
}
}
}

View File

@@ -960,6 +960,24 @@ Singleton {
"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: {
"none": 0,
"short": 250,
@@ -1084,7 +1102,7 @@ Singleton {
property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.getEffectiveFontFamily();
return GreetdSettings.fontFamily;
}
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
}
@@ -1551,7 +1569,7 @@ Singleton {
if (typeof SettingsData !== "undefined") {
const skipTemplates = [];
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 {
if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk");
@@ -1595,8 +1613,6 @@ Singleton {
skipTemplates.push("vscode");
if (!SettingsData.matugenTemplateEmacs)
skipTemplates.push("emacs");
if (!SettingsData.matugenTemplateZed)
skipTemplates.push("zed");
}
if (skipTemplates.length > 0) {
args.push("--skip-templates", skipTemplates.join(","));
@@ -1989,7 +2005,7 @@ Singleton {
FileView {
id: dynamicColorsFileView
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";
return colorsPath;
}

View File

@@ -10,352 +10,22 @@ Singleton {
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() {
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() {
detectAuthCapabilities();
fprintdDetectionProcess.running = true;
}
function detectU2f() {
detectAuthCapabilities();
u2fDetectionProcess.running = true;
}
function checkPluginSettings() {
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 {
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
@@ -365,15 +35,15 @@ Singleton {
if (!settingsRoot)
return;
if (text && text.trim()) {
const lines = text.trim().split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith("qt5ct:")) {
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith("qt6ct:")) {
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith("gtk:")) {
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
var lines = text.trim().split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.startsWith('qt5ct:')) {
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
} else if (line.startsWith('qt6ct:')) {
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
} else if (line.startsWith('gtk:')) {
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
}
}
}
@@ -381,181 +51,23 @@ Singleton {
}
}
property var fingerprintProbeProcess: 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"]
property var fprintdDetectionProcess: Process {
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.fingerprintProbeOutput = text || "";
root.fingerprintProbeStreamFinished = true;
root.finalizeFingerprintProbe();
}
}
onExited: function (exitCode) {
root.fingerprintProbeExitCode = exitCode;
root.fingerprintProbeExited = true;
root.finalizeFingerprintProbe();
if (!settingsRoot)
return;
settingsRoot.fprintdAvailable = (exitCode === 0);
}
}
property var pamSupportDetectionProcess: Process {
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
property var u2fDetectionProcess: Process {
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.pamSupportProbeOutput = text || "";
root.pamSupportProbeStreamFinished = true;
root.finalizePamSupportProbe();
}
}
onExited: function (exitCode) {
root.pamSupportProbeExitCode = exitCode;
root.pamSupportProbeExited = true;
root.finalizePamSupportProbe();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.greetdPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.loginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: dankshellU2fPamWatcher
path: "/etc/pam.d/dankshell-u2f"
printErrors: false
onLoaded: {
root.dankshellU2fPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.dankshellU2fPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: u2fKeysWatcher
path: root.u2fKeysPath
printErrors: false
onLoaded: {
root.u2fKeysText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.u2fKeysText = "";
root.recomputeAuthCapabilities();
if (!settingsRoot)
return;
settingsRoot.u2fAvailable = (exitCode === 0);
}
}

View File

@@ -47,6 +47,9 @@ var SPEC = {
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 },
@@ -123,7 +126,6 @@ var SPEC = {
showOccupiedWorkspacesOnly: { def: false },
reverseScrolling: { def: false },
dwlShowAllTags: { def: false },
workspaceActiveAppHighlightEnabled: { def: false },
workspaceColorMode: { def: "default" },
workspaceOccupiedColorMode: { def: "none" },
workspaceUnfocusedColorMode: { def: "default" },
@@ -165,17 +167,6 @@ var SPEC = {
centeringMode: { def: "index" },
clockDateFormat: { 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 },
appLauncherViewMode: { def: "list" },
@@ -284,26 +275,17 @@ var SPEC = {
matugenTemplateKitty: { def: true },
matugenTemplateFoot: { def: true },
matugenTemplateAlacritty: { def: true },
matugenTemplateNeovim: { def: false },
matugenTemplateNeovim: { def: true },
matugenTemplateWezterm: { def: true },
matugenTemplateDgop: { def: true },
matugenTemplateKcolorscheme: { def: true },
matugenTemplateVscode: { def: true },
matugenTemplateEmacs: { def: true },
matugenTemplateZed: { def: true },
matugenTemplateNeovimSettings: {
def: {
dark: { baseTheme: "github_dark", harmony: 0.5 },
light: { baseTheme: "github_light", harmony: 0.5 }
}
},
showDock: { def: false },
dockAutoHide: { def: false },
dockSmartAutoHide: { def: false },
dockGroupByApp: { def: false },
dockRestoreSpecialWorkspaceOnClick: { def: false },
dockOpenOnOverview: { def: false },
dockPosition: { def: 1 },
dockSpacing: { def: 4 },
@@ -348,23 +330,9 @@ var SPEC = {
enableFprint: { def: false },
maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false },
lockFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintCanEnable: { def: false, persist: false },
greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false },
u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false },
lockU2fReady: { def: false, persist: false },
lockU2fReason: { def: "probe_failed", persist: false },
greeterU2fCanEnable: { def: false, persist: false },
greeterU2fReady: { def: false, persist: false },
greeterU2fReason: { def: "probe_failed", persist: false },
greeterU2fSource: { def: "none", persist: false },
lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 },
@@ -387,7 +355,6 @@ var SPEC = {
notificationHistorySaveNormal: { def: true },
notificationHistorySaveCritical: { def: true },
notificationRules: { def: [] },
notificationFocusedMonitor: { def: false },
osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },

View File

@@ -1,5 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Services.Greetd
import qs.Common
import qs.Modules.Greetd
Scope {

View File

@@ -313,7 +313,7 @@ Item {
}
Variants {
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
model: SettingsData.getFilteredScreens("notifications")
delegate: NotificationPopupManager {
modelData: item

View File

@@ -26,10 +26,10 @@ Item {
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
property list<real> animationExitCurve: Theme.variantModalExitCurve
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
@@ -44,11 +44,13 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened
signal dialogClosed
@@ -58,19 +60,34 @@ Item {
function open() {
closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
contentWindow.screen = focusedScreen;
if (!useSingleWindow)
clickCatcher.screen = focusedScreen;
}
if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(root);
shouldBeVisible = true;
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
}
function close() {
@@ -131,7 +148,7 @@ Item {
Timer {
id: closeTimer
interval: animationDuration + 50
interval: Theme.variantCloseInterval(animationDuration)
onTriggered: {
if (shouldBeVisible)
return;
@@ -145,7 +162,17 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
readonly property real 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 alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -205,9 +232,26 @@ Item {
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
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 {
@@ -250,9 +294,12 @@ Item {
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 {
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
left: actualMarginLeft
top: actualMarginTop
right: 0
bottom: 0
}
@@ -282,13 +329,14 @@ Item {
anchors.fill: parent
z: -1
color: "black"
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
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
}
}
@@ -296,8 +344,8 @@ Item {
Item {
id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
width: root.alignedWidth
height: root.alignedHeight
@@ -313,45 +361,117 @@ Item {
}
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real offsetX: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return 15;
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 {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -367,15 +487,14 @@ Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: root.shouldBeVisible ? 1 : 0
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation {
duration: animationDuration
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}

View File

@@ -1006,7 +1006,9 @@ Item {
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = getFirstItemIndex();
if (selectedFlatIndex >= flatModel.length) {
selectedFlatIndex = getFirstItemIndex();
}
updateSelectedItem();
});
}

View File

@@ -14,6 +14,7 @@ Item {
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
@@ -23,8 +24,14 @@ Item {
property string _pendingMode: ""
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 var effectiveScreen: launcherWindow.screen
readonly property var effectiveScreen: contentWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -78,6 +85,34 @@ Item {
}
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
function _ensureContentLoadedAndInitialize(query, mode) {
@@ -97,7 +132,8 @@ Item {
if (!spotlightContent)
return;
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) {
spotlightContent.searchField.text = query;
@@ -130,40 +166,59 @@ Item {
}
}
function show() {
function _openCommon(query, mode) {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
// Disable animations so the snap is instant
animationsEnabled = false;
spotlightOpen = true;
keyboardActive = true;
// Freeze the collapsed offsets (they depend on height which could change)
_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);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab)
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) {
closeCleanupTimer.stop();
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, "");
_openCommon(query, "");
}
function hide() {
@@ -171,13 +226,17 @@ Item {
return;
openedFromOverview = false;
isClosing = true;
contentVisible = false;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(root);
closeCleanupTimer.start();
}
@@ -186,21 +245,7 @@ Item {
}
function showWithMode(mode) {
closeCleanupTimer.stop();
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);
_openCommon("", mode);
}
function toggleWithMode(mode) {
@@ -221,10 +266,13 @@ Item {
Timer {
id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
repeat: false
onTriggered: {
isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
@@ -242,7 +290,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
windows: [contentWindow]
active: false
onCleared: {
@@ -267,7 +315,7 @@ Item {
if (Quickshell.screens.length === 0)
return;
const screen = launcherWindow.screen;
const screen = contentWindow.screen;
const screenName = screen?.name;
let needsReset = !screen || !screenName;
@@ -289,35 +337,31 @@ Item {
return;
root._windowEnabled = false;
launcherWindow.screen = newScreen;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
}
}
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
PanelWindow {
id: launcherWindow
visible: root._windowEnabled && (spotlightOpen || isClosing)
id: backgroundWindow
visible: false
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
}
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
@@ -327,11 +371,11 @@ Item {
}
mask: Region {
item: spotlightOpen ? fullScreenMask : null
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
}
Item {
id: fullScreenMask
id: bgFullScreenMask
anchors.fill: parent
}
@@ -339,13 +383,14 @@ Item {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: launcherMotionVisible || opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
}
@@ -353,88 +398,240 @@ Item {
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: mouse => {
var contentX = modalContainer.x;
var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
onClicked: root.hide()
}
}
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: modalContainer
x: root.modalX
y: root.modalY
width: root.modalWidth
height: root.modalHeight
visible: contentVisible || opacity > 0
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scale {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
ElevationShadow {
id: launcherShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
fallbackOffset: 6
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
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 {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 2000
clip: shouldClip
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
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 {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
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 {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
}

View File

@@ -86,7 +86,7 @@ FocusScope {
Controller {
id: controller
active: root.parentModal?.spotlightOpen ?? true
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: root.viewModeContext
onItemExecuted: {
@@ -462,7 +462,7 @@ FocusScope {
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
placeholderText: ""
ignoreUpDownKeys: true
ignoreTabKeys: true
@@ -548,7 +548,6 @@ FocusScope {
}
}
}
}
Item {
@@ -697,7 +696,13 @@ FocusScope {
Item {
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)
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 {
id: resultsList

View File

@@ -468,7 +468,7 @@ Item {
switch (mode) {
case "files":
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)
return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2)

View File

@@ -1,6 +1,5 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
@@ -12,45 +11,8 @@ FloatingWindow {
property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false
property bool awaitingFprintForPassword: false
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() {
passwordField.forceActiveFocus();
}
@@ -58,7 +20,6 @@ FloatingWindow {
function show() {
passwordInput = "";
isLoading = false;
awaitingFprintForPassword = false;
visible = true;
Qt.callLater(focusPasswordField);
}
@@ -67,27 +28,17 @@ FloatingWindow {
visible = false;
}
function _commitSubmit() {
isLoading = true;
awaitingFprintForPassword = false;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function submitAuth() {
if (!currentFlow || isLoading)
return;
if (!currentFlow.isResponseRequired) {
awaitingFprintForPassword = true;
return;
}
_commitSubmit();
isLoading = true;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function cancelAuth() {
if (isLoading)
return;
awaitingFprintForPassword = false;
if (currentFlow) {
currentFlow.cancelAuthenticationRequest();
return;
@@ -109,7 +60,6 @@ FloatingWindow {
}
passwordInput = "";
isLoading = false;
awaitingFprintForPassword = false;
}
Connections {
@@ -133,11 +83,6 @@ FloatingWindow {
function onIsResponseRequiredChanged() {
if (!currentFlow.isResponseRequired)
return;
if (awaitingFprintForPassword && passwordInput !== "") {
_commitSubmit();
return;
}
awaitingFprintForPassword = false;
isLoading = false;
passwordInput = "";
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 {
id: contentFocusScope
@@ -295,30 +205,36 @@ FloatingWindow {
visible: text !== ""
}
DankTextField {
id: passwordField
Rectangle {
width: parent.width
height: inputFieldHeight
backgroundColor: Theme.surfaceHover
normalBorderColor: Theme.outlineStrong
focusedBorderColor: Theme.primary
borderWidth: 1
focusedBorderWidth: 2
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
leftIconSize: 20
leftIconColor: Theme.primary
leftIconFocusedColor: Theme.primary
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
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: ""
enabled: !isLoading
onTextEdited: passwordInput = text
onAccepted: submitAuth()
MouseArea {
anchors.fill: parent
enabled: !isLoading
onClicked: passwordField.forceActiveFocus()
}
DankTextField {
id: passwordField
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: passwordInput = text
onAccepted: submitAuth()
}
}
StyledText {

View File

@@ -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 {
id: pluginsLoader
anchors.fill: parent
@@ -485,7 +470,7 @@ FocusScope {
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
Qt.callLater(() => item.forceActiveFocus());
}
}
@@ -500,7 +485,7 @@ FocusScope {
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
Qt.callLater(() => item.forceActiveFocus());
}
}
}

View File

@@ -287,12 +287,6 @@ Rectangle {
"icon": "lock",
"tabIndex": 11
},
{
"id": "greeter",
"text": I18n.tr("Greeter"),
"icon": "login",
"tabIndex": 31
},
{
"id": "power_sleep",
"text": I18n.tr("Power & Sleep"),

View File

@@ -8,9 +8,6 @@ DankPopout {
layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: ""
property string _pendingQuery: ""
@@ -44,35 +41,8 @@ DankPopout {
openWithQuery(query);
}
readonly property int _baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
popupWidth: 560
popupHeight: 640
triggerWidth: 40
positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -136,7 +106,7 @@ DankPopout {
QtObject {
id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: false
property bool isClosing: appDrawerPopout.isClosing
function hide() {
appDrawerPopout.close();

View File

@@ -12,7 +12,7 @@ DankPopout {
id: root
layerNamespace: "dms:control-center"
fullHeightSurface: true
fullHeightSurface: false
property string expandedSection: ""
property var triggerScreen: null
@@ -126,9 +126,11 @@ DankPopout {
z: 5000
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
}

View File

@@ -1117,6 +1117,7 @@ Item {
if (!notificationCenterLoader.item) {
return;
}
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) {

View File

@@ -390,11 +390,10 @@ BasePill {
anchors.top: parent.top
}
NumericText {
StyledText {
id: audioPercentV
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
@@ -417,11 +416,10 @@ BasePill {
anchors.top: parent.top
}
NumericText {
StyledText {
id: micPercentV
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
@@ -444,11 +442,10 @@ BasePill {
anchors.top: parent.top
}
NumericText {
StyledText {
id: brightnessPercentV
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
@@ -539,8 +536,7 @@ BasePill {
}
Rectangle {
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4
implicitWidth: width
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
@@ -556,23 +552,20 @@ BasePill {
anchors.leftMargin: 2
}
NumericText {
StyledText {
id: audioPercent
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
Rectangle {
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4
implicitWidth: width
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
@@ -588,22 +581,20 @@ BasePill {
anchors.leftMargin: 2
}
NumericText {
StyledText {
id: micPercent
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: micIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
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
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
@@ -619,17 +610,15 @@ BasePill {
anchors.leftMargin: 2
}
NumericText {
StyledText {
id: brightnessPercent
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: brightnessIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}

View File

@@ -211,17 +211,16 @@ BasePill {
text: {
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text;
if (compactMode && title === appName) {
return title;
}
if (!title || !appName) {
return title;
}
if (title.endsWith(" - " + appName)) {
return title.substring(0, title.length - (" - " + appName).length);
}
if (title.endsWith(appName)) {
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
}
return title;

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
@@ -161,23 +162,6 @@ BasePill {
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 var currentTrayMenu: null
@@ -956,6 +940,13 @@ BasePill {
}
})(), 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
scale: root.menuOpen ? 1 : 0.85
@@ -976,14 +967,19 @@ BasePill {
ElevationShadow {
id: bgShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
direction: root.effectiveShadowDirection
fallbackOffset: 6
level: menuContainer.elev
fallbackOffset: 4
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)
targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true
sourceRect.smooth: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
shadowEnabled: Theme.elevationEnabled
layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1406,6 +1402,13 @@ BasePill {
}
})(), 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
scale: menuRoot.showMenu ? 1 : 0.85
@@ -1426,13 +1429,18 @@ BasePill {
ElevationShadow {
id: menuBgShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
direction: root.effectiveShadowDirection
fallbackOffset: 6
level: menuContainer.elev
fallbackOffset: 4
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)
targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
shadowEnabled: Theme.elevationEnabled
layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically

View File

@@ -1470,10 +1470,6 @@ Item {
delegate: Item {
width: 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 {
id: rowAppIcon
@@ -1489,14 +1485,14 @@ Item {
color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: appBorderColor
opacity: appOpacity
border.color: Theme.primarySelected
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
StyledText {
anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45
color: appGlyphColor
color: Theme.primary
font.weight: Font.Bold
}
}
@@ -1507,14 +1503,14 @@ Item {
color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: appBorderColor
opacity: appOpacity
border.color: Theme.primarySelected
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
DankIcon {
anchors.centerIn: parent
size: parent.width * 0.7
name: "sports_esports"
color: appGlyphColor
color: Theme.primary
}
}
@@ -1527,12 +1523,11 @@ Item {
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
}
}
IconImage {
id: rowSteamIcon
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1543,21 +1538,11 @@ Item {
anchors.centerIn: parent
size: root.appIconSize
name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
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 {
id: rowAppMouseArea
anchors.fill: parent
@@ -1639,10 +1624,6 @@ Item {
delegate: Item {
width: 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 {
id: colAppIcon
@@ -1658,14 +1639,14 @@ Item {
color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: appBorderColor
opacity: appOpacity
border.color: Theme.primarySelected
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
StyledText {
anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45
color: appGlyphColor
color: Theme.primary
font.weight: Font.Bold
}
}
@@ -1676,14 +1657,14 @@ Item {
color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: appBorderColor
opacity: appOpacity
border.color: Theme.primarySelected
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
DankIcon {
anchors.centerIn: parent
size: parent.width * 0.7
name: "sports_esports"
color: appGlyphColor
color: Theme.primary
}
}
@@ -1696,12 +1677,11 @@ Item {
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
}
}
IconImage {
id: colSteamIcon
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1712,21 +1692,11 @@ Item {
anchors.centerIn: parent
size: root.appIconSize
name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
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 {
id: colAppMouseArea
anchors.fill: parent

View File

@@ -16,7 +16,6 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80
screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false
property bool __contentReady: false

View File

@@ -44,6 +44,43 @@ Item {
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() {
__volumeHoverCount++;
panelEntered();
@@ -62,30 +99,47 @@ Item {
visible: dropdownType === 1 && volumeAvailable
width: 60
height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
radius: Theme.cornerRadius * 2
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.width: 1
opacity: dropdownType === 1 ? 1 : 0
scale: dropdownType === 1 ? 1 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
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 {
id: audioDevicesPanel
visible: dropdownType === 2
visible: dropdownType === 2 && activePlayer !== null
width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
radius: Theme.cornerRadius * 2
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.width: 2
opacity: dropdownType === 2 ? 1 : 0
scale: dropdownType === 2 ? 1 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
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
width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
radius: Theme.cornerRadius * 2
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.width: 2
opacity: dropdownType === 3 ? 1 : 0
scale: dropdownType === 3 ? 1 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
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
}
}

View File

@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
@@ -134,40 +133,6 @@ Item {
function getGroupedToplevels() {
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: {
if (mouseArea.pressed || dragging)
return;
@@ -311,11 +276,8 @@ Item {
break;
case "window":
const windowToplevel = getToplevelObject();
if (windowToplevel) {
if (restoreSpecialWorkspaceWindow(windowToplevel))
return;
if (windowToplevel)
windowToplevel.activate();
}
break;
case "grouped":
if (appData.windowCount === 0) {
@@ -338,11 +300,8 @@ Item {
SessionService.launchDesktopEntry(groupedEntry);
} else if (appData.windowCount === 1) {
const groupedToplevel = getToplevelObject();
if (groupedToplevel) {
if (restoreSpecialWorkspaceWindow(groupedToplevel))
return;
if (groupedToplevel)
groupedToplevel.activate();
}
} else if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);

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;
}

View File

@@ -4,16 +4,13 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv
Singleton {
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 memoryFile: greetCfgDir + "/.local/state/memory.json"
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
readonly property string memoryFile: greetCfgDir + "/memory.json"
property string lastSessionId: ""
property string lastSuccessfulUser: ""
@@ -52,44 +49,26 @@ Singleton {
if (!content || !content.trim())
return;
const memory = JSON.parse(content);
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
lastSessionId = memory.lastSessionId || "";
lastSuccessfulUser = memory.lastSuccessfulUser || "";
} catch (e) {
console.warn("Failed to parse greetd memory:", e);
}
}
function saveMemory() {
let memory = {};
if (rememberLastSession && lastSessionId)
memory.lastSessionId = lastSessionId;
if (rememberLastUser && lastSuccessfulUser)
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
memoryFileView.setText(JSON.stringify({
"lastSessionId": lastSessionId,
"lastSuccessfulUser": lastSuccessfulUser
}, null, 2));
}
function setLastSessionId(id) {
if (!rememberLastSession) {
if (lastSessionId !== "") {
lastSessionId = "";
saveMemory();
}
return;
}
lastSessionId = id || "";
saveMemory();
}
function setLastSuccessfulUser(username) {
if (!rememberLastUser) {
if (lastSuccessfulUser !== "") {
lastSuccessfulUser = "";
saveMemory();
}
return;
}
lastSuccessfulUser = username || "";
saveMemory();
}

View File

@@ -5,22 +5,15 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import "GreetdEnv.js" as GreetdEnv
Singleton {
id: root
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";
}
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 bool settingsLoaded: false
property string customThemeFile: ""
@@ -28,12 +21,6 @@ Singleton {
property bool use24HourClock: true
property bool showSeconds: 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 nightModeEnabled: false
property string weatherLocation: "New York, NY"
@@ -54,11 +41,6 @@ Singleton {
property string lockDateFormat: ""
property bool lockScreenShowPowerActions: 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 real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -70,103 +52,66 @@ Singleton {
function parseSettings(content) {
try {
let settings = {};
if (content && content.trim()) {
settings = JSON.parse(content);
}
const settings = JSON.parse(content);
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
settingsLoaded = true;
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";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : 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;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : 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;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
}
Theme.applyGreeterTheme(currentThemeName);
}
Theme.applyGreeterTheme(currentThemeName);
}
} catch (e) {
console.warn("Failed to parse greetd settings:", e);
} finally {
settingsLoaded = true;
}
}
function getEffectiveTimeFormat() {
const use24 = greeterUse24HourClock;
const secs = greeterShowSeconds;
const pad = greeterPadHours12Hour;
if (use24)
return secs ? "hh:mm:ss" : "hh:mm";
if (pad)
return secs ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "h:mm:ss AP" : "h:mm AP";
if (use24HourClock)
return showSeconds ? "hh:mm:ss" : "hh:mm";
if (padHours12Hour)
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
}
function getEffectiveLockDateFormat() {
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
}
function getEffectiveWallpaperFillMode() {
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
}
function getEffectiveFontFamily() {
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
}
function getFilteredScreens(componentId) {
@@ -188,9 +133,5 @@ Singleton {
onLoaded: {
parseSettings(settingsFile.text());
}
onLoadFailed: error => {
console.warn("Failed to load greetd settings:", error);
root.parseSettings("");
}
}
}

View File

@@ -31,38 +31,6 @@ Item {
signal launchRequested
property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property bool pendingPasswordResponse: false
property bool passwordSubmitRequested: false
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 10000
property int externalAuthTimeoutMs: 30000
property int memoryFlushDelayMs: 120
property string pendingLaunchCommand: ""
property var pendingLaunchEnv: []
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string faillockConfigText: ""
property bool greeterWallpaperOverrideExists: false
property string externalAuthAutoStartedForUser: ""
property int passwordSessionTransitionRetryCount: 0
property int maxPasswordSessionTransitionRetries: 2
property bool fprintdProbeComplete: false
property bool fprintdHasDevice: false
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
function initWeatherService() {
if (weatherInitialized)
@@ -76,492 +44,34 @@ Item {
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 {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) {
if (GreetdSettings.settingsLoaded)
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: {
initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen)
applyLastSuccessfulUser();
if (CompositorService.isHyprland)
updateHyprlandLayout();
fprintdDeviceProbe.running = true;
}
function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser);
maybeAutoStartExternalAuth();
}
}
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
maybeAutoStartExternalAuth();
}
function submitBufferedPassword() {
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
Greetd.respond(GreeterState.passwordBuffer || "");
GreeterState.passwordBuffer = "";
inputField.text = "";
return true;
}
function requestPasswordSessionTransition() {
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && !hasPasswordBuffer)
return;
if (cancelingExternalAuthForPassword)
return;
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
pendingPasswordResponse = false;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
resetPasswordSessionTransition(true);
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
return;
}
cancelingExternalAuthForPassword = true;
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
Greetd.cancelSession();
}
function startAuthSession(submitPassword) {
submitPassword = submitPassword === true;
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking)
return;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (Greetd.state !== GreetdState.Inactive) {
if (pendingPasswordResponse && submitPassword)
submitBufferedPassword();
else if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (cancelingExternalAuthForPassword) {
if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
return;
pendingPasswordResponse = false;
passwordSubmitRequested = submitPassword;
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
// Use greeterExternalAuthAvailable so systems with pam_fprintd but no hardware don't incur the 30 s wait.
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterExternalAuthAvailable;
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function maybeAutoStartExternalAuth() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (!root.greeterExternalAuthAvailable)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
return;
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
return;
if (externalAuthAutoStartedForUser === GreeterState.username)
return;
externalAuthAutoStartedForUser = GreeterState.username;
startAuthSession(false);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef();
@@ -603,34 +113,6 @@ Item {
}
}
// Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives.
Process {
id: fprintdDeviceProbe
running: false
// sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
// keeping the PAM-only fallback active in those cases.
command: ["sh", "-c",
"command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
"gdbus call --system " +
"--dest net.reactivated.Fprint " +
"--object-path /net/reactivated/Fprint/Manager " +
"--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
"|| echo PROBE_UNAVAILABLE"]
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("PROBE_UNAVAILABLE"))
return; // PAM-only fallback stays active
root.fprintdHasDevice = text.includes("objectpath");
root.fprintdProbeComplete = true;
root.maybeAutoStartExternalAuth();
}
}
onExited: function(exitCode, exitStatus) {
if (!root.fprintdProbeComplete)
root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
}
}
Connections {
target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland
@@ -661,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 {
anchors.fill: parent
screenName: root.screenName
visible: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return false;
var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
@@ -706,14 +159,12 @@ Item {
anchors.fill: parent
source: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
}
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
smooth: true
asynchronous: false
cache: true
@@ -876,7 +327,10 @@ Item {
anchors.top: clockContainer.bottom
anchors.topMargin: 4
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
color: "white"
@@ -945,9 +399,6 @@ Item {
if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width;
}
if (externalAuthButton.visible) {
margin += externalAuthButton.width;
}
if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width;
}
@@ -964,18 +415,21 @@ Item {
return;
if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text;
if (!text || text.length === 0)
root.passwordSubmitRequested = false;
} else {
GreeterState.usernameInput = text;
}
}
onAccepted: {
if (GreeterState.showPasswordInput) {
root.startAuthSession(true);
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
} else {
if (text.trim()) {
root.submitUsername(text);
GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true;
text = "";
syncingFromState = false;
@@ -1007,14 +461,14 @@ Item {
anchors.left: lockIcon.right
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.verticalCenter: parent.verticalCenter
text: {
if (GreeterState.unlocking) {
return "Logging in...";
}
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
if (Greetd.state !== GreetdState.Inactive) {
return "Authenticating...";
}
if (GreeterState.showPasswordInput) {
@@ -1022,7 +476,7 @@ Item {
}
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
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
@@ -1044,7 +498,7 @@ Item {
StyledText {
anchors.left: lockIcon.right
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.verticalCenter: parent.verticalCenter
text: {
@@ -1074,27 +528,15 @@ Item {
DankActionButton {
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.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
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
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 {
id: virtualKeyboardButton
@@ -1103,7 +545,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: 32
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: visible
onClicked: {
if (keyboard_controller.isKeyboardActive) {
@@ -1122,14 +564,19 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return"
buttonSize: 36
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: true
onClicked: {
if (GreeterState.showPasswordInput) {
root.startAuthSession(true);
if (GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
} else {
if (inputField.text.trim()) {
root.submitUsername(inputField.text);
GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = "";
}
}
@@ -1154,16 +601,20 @@ Item {
StyledText {
Layout.fillWidth: true
Layout.preferredHeight: 38
Layout.preferredHeight: 20
Layout.topMargin: -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
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
opacity: GreeterState.pamState !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation {
@@ -1216,7 +667,6 @@ Item {
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: {
GreeterState.reset();
root.externalAuthAutoStartedForUser = "";
inputField.text = "";
PortalService.profileImage = "";
}
@@ -1579,11 +1029,9 @@ Item {
return;
if (!GreetdMemory.memoryReady)
return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
if (savedSession && GreetdSettings.rememberLastSession) {
const savedSession = GreetdMemory.lastSessionId;
if (savedSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i;
@@ -1716,151 +1164,44 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) {
cancelingExternalAuthForPassword = false;
passwordSessionTransitionRetryCount = 0;
awaitingExternalAuth = false;
pendingPasswordResponse = true;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && hasPasswordBuffer)
passwordSubmitRequested = true;
if (passwordSubmitRequested && !root.submitBufferedPassword())
passwordSubmitRequested = false;
if (passwordSubmitRequested || hasPasswordBuffer) {
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
} else {
authTimeout.stop();
}
Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "";
inputField.text = "";
return;
}
pendingPasswordResponse = false;
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
if (!passwordSubmitRequested)
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
authTimeout.interval = externalAuthTimeoutMs;
else
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond("");
}
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);
}
if (!error)
Greetd.respond("");
}
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 sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
if (!sessionCmd) {
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
return;
}
GreeterState.unlocking = true;
launchTimeout.restart();
if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSessionId(sessionPath);
} else if (GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
memoryFlushTimer.restart();
GreetdMemory.setLastSessionId(sessionPath);
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
}
function onAuthFailure(message) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop();
GreeterState.unlocking = false;
if (isLikelyLockoutMessage(message)) {
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
GreeterState.pamState = "fail";
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
function onError(error) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop();
GreeterState.unlocking = false;
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 = "";
inputField.text = "";
placeholderDelay.restart();
@@ -1874,11 +1215,8 @@ Item {
onTriggered: {
if (!GreeterState.unlocking)
return;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
GreeterState.unlocking = false;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
}
@@ -1887,7 +1225,7 @@ Item {
Timer {
id: placeholderDelay
interval: 4000
onTriggered: clearAuthFeedback()
onTriggered: GreeterState.pamState = ""
}
LockPowerMenu {

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.
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
- **Session Memory**: Remembers last selected session and user
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
## Installation
@@ -213,7 +212,6 @@ dms-greeter --command hyprland
dms-greeter --command sway
dms-greeter --command mangowc
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`:

View File

@@ -6,9 +6,6 @@ COMPOSITOR=""
COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() {
cat << EOF
@@ -25,15 +22,6 @@ Options:
(default: dms-greeter)
--cache-dir PATH Cache directory for greeter data
(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
Examples:
@@ -42,7 +30,6 @@ Examples:
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 niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango
dms-greeter --command labwc
EOF
@@ -56,41 +43,6 @@ require_command() {
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
case $1 in
--command)
@@ -109,26 +61,6 @@ while [[ $# -gt 0 ]]; do
CACHE_DIR="$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)
show_help
exit 0
@@ -179,71 +111,9 @@ export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
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"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Propagate correct XDG dirs into the systemd user session so socket-activated
# services (e.g. wireplumber) don't inherit HOME=/ from /etc/passwd.
if command -v systemctl >/dev/null 2>&1; then
systemctl --user set-environment \
HOME="$CACHE_DIR" \
XDG_STATE_HOME="$CACHE_DIR/.local/state" \
XDG_DATA_HOME="$CACHE_DIR/.local/share" \
XDG_CACHE_HOME="$CACHE_DIR/.cache" 2>/dev/null || true
if systemctl --user is-active --quiet wireplumber.service 2>/dev/null; then
systemctl --user restart wireplumber.service 2>/dev/null || true
fi
fi
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
mkdir -p "$CACHE_DIR"
if command -v qs >/dev/null 2>&1; then
QS_BIN="qs"
@@ -260,9 +130,7 @@ if [[ "$DMS_PATH" == /* ]]; then
else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
if [[ "$DEBUG_MODE" == "1" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
echo "Located DMS config at: $RESOLVED_PATH" >&2
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
@@ -283,10 +151,6 @@ hotkey-overlay {
environment {
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 {
@@ -328,7 +192,7 @@ NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
exec niri -c "$COMPOSITOR_CONFIG"
;;
hyprland)
@@ -340,16 +204,11 @@ NIRI_EOF
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
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 {
disable_hyprland_logo = true
}
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -358,19 +217,14 @@ HYPRLAND_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
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
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
exec Hyprland -c "$COMPOSITOR_CONFIG"
fi
;;
@@ -379,7 +233,6 @@ HYPRLAND_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
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"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -388,12 +241,11 @@ SWAY_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
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"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;;
scroll)
@@ -401,7 +253,6 @@ SWAY_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
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"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -410,12 +261,11 @@ SCROLL_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
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"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
exec scroll -c "$COMPOSITOR_CONFIG"
;;
miracle|miracle-wm)
@@ -435,24 +285,24 @@ exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
exec miracle-wm -c "$COMPOSITOR_CONFIG"
;;
labwc)
require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else
exec_compositor "labwc" labwc --session "$QS_CMD"
exec labwc --session "$QS_CMD"
fi
;;
mango|mangowc)
require_command "mango"
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
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
exec mango -s "$QS_CMD && mmsg -d quit"
fi
;;

View File

@@ -755,7 +755,7 @@ Item {
}
}
onAccepted: {
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
pam.passwd.start();
}
}
@@ -764,11 +764,6 @@ Item {
return;
}
if (root.unlocking) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
if (pam.u2fPending) {
pam.cancelU2fPending();
@@ -1022,7 +1017,7 @@ Item {
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
enabled: !demoMode
onClicked: {
if (!demoMode && !root.unlocking && !pam.u2fPending) {
if (!demoMode && !pam.u2fPending) {
pam.passwd.start();
}
}
@@ -1631,7 +1626,6 @@ Item {
onStateChanged: {
root.pamState = state;
if (state !== "") {
root.unlocking = false;
placeholderDelay.restart();
passwordField.text = "";
root.passwordBuffer = "";
@@ -1647,15 +1641,6 @@ Item {
}
}
Connections {
target: pam
function onUnlockInProgressChanged() {
if (!pam.unlockInProgress && root.unlocking)
root.unlocking = false;
}
}
Binding {
target: pam
property: "buffer"

View File

@@ -25,29 +25,6 @@ Scope {
signal flashMsg
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 {
if (!unlockInProgress) {
unlockInProgress = true;
@@ -59,7 +36,6 @@ Scope {
u2fPendingTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockRequestTimeout.restart();
unlockRequested();
}
}
@@ -90,13 +66,6 @@ Scope {
printErrors: false
}
FileView {
id: loginConfigWatcher
path: "/etc/pam.d/login"
printErrors: false
}
FileView {
id: u2fConfigWatcher
@@ -108,7 +77,7 @@ Scope {
id: passwd
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: {
if (message.startsWith("The account is locked"))
@@ -133,13 +102,6 @@ Scope {
return;
}
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error)
root.state = "error";
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 {
id: fprint
property bool available: SettingsData.lockFingerprintReady
property bool available
property int tries
property int errorTries
@@ -223,7 +173,7 @@ Scope {
PamContext {
id: u2f
property bool available: SettingsData.lockU2fReady
property bool available
function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
@@ -252,7 +202,7 @@ Scope {
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: {
if (message !== "")
if (message.toLowerCase().includes("touch"))
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 {
id: errorRetry
@@ -309,26 +279,6 @@ Scope {
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 {
id: stateReset
@@ -351,17 +301,24 @@ Scope {
onLockSecuredChanged: {
if (lockSecured) {
SettingsData.refreshAuthAvailability();
availProc.running = true;
u2fAvailProc.running = true;
root.state = "";
root.fprintState = "";
root.u2fState = "";
root.u2fPending = false;
root.lockMessage = "";
root.resetAuthFlows();
fprint.checkAvail();
u2f.checkAvail();
root.unlockInProgress = false;
} 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();
}
function onLockFingerprintReadyChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void {
u2f.checkAvail();
}
function onLockU2fReadyChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void {
if (root.lockSecured) {
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
u2f.checkAvail();

View File

@@ -7,7 +7,7 @@ DankPopout {
id: root
layerNamespace: "dms:notification-center-popout"
fullHeightSurface: true
fullHeightSurface: false
property bool notificationHistoryVisible: false
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
positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false
screen: triggerScreen

View File

@@ -24,6 +24,29 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 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")
property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -137,9 +160,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying
NumberAnimation {
id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
@@ -911,9 +934,9 @@ PanelWindow {
if (isCenterPosition)
return 0;
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"
from: {
if (isTopCenter)
return -Anims.slidePx;
return -entryTravel;
if (isBottomCenter)
return Anims.slidePx;
return entryTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
return isLeft ? -entryTravel : entryTravel;
}
to: 0
duration: Theme.notificationEnterDuration
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
easing.type: Easing.BezierSpline
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
easing.bezierCurve: Theme.variantPopoutEnterCurve
onStopped: {
if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) {
@@ -959,35 +982,35 @@ PanelWindow {
from: 0
to: {
if (isTopCenter)
return -Anims.slidePx;
return -exitTravel;
if (isBottomCenter)
return Anims.slidePx;
return exitTravel;
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.bezierCurve: Theme.expressiveCurves.emphasizedAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
NumberAnimation {
target: content
property: "opacity"
from: 1
to: 0
duration: Theme.notificationExitDuration
to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standardAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
NumberAnimation {
target: content
property: "scale"
from: 1
to: 0.98
duration: Theme.notificationExitDuration
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
easing.bezierCurve: Theme.variantPopoutExitCurve
}
}

View File

@@ -108,13 +108,6 @@ QtObject {
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) {
for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting)
@@ -125,7 +118,7 @@ QtObject {
}
}
for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen())
if (w && !_hasWindowFor(w))
_insertAtTop(w);
}
}

View File

@@ -909,9 +909,6 @@ Singleton {
case "dwl":
DwlService.generateOutputsConfig(outputsData);
break;
default:
WlrOutputService.applyOutputsConfig(outputsData, outputs);
break;
}
}

View File

@@ -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 {
width: parent.width
text: I18n.tr("Show on Last Display")

View File

@@ -160,16 +160,6 @@ Item {
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 {
settingKey: "dockIndicatorStyle"
tags: ["dock", "indicator", "style", "circle", "line"]

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
}
}
}
}
}
}

View File

@@ -9,51 +9,6 @@ import qs.Modules.Settings.Widgets
Item {
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 {
id: videoBrowserModal
browserTitle: I18n.tr("Select Video or Folder")
@@ -217,10 +172,10 @@ Item {
settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
text: I18n.tr("Enable fingerprint authentication")
description: root.lockFingerprintDescription()
descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
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.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableFprint
enabled: root.lockFprintToggleAvailable
enabled: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.set("enableFprint", checked)
}
@@ -228,10 +183,10 @@ Item {
settingKey: "enableU2f"
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")
description: root.lockU2fDescription()
descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
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.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableU2f
enabled: root.lockU2fToggleAvailable
enabled: SettingsData.u2fAvailable
onToggled: checked => SettingsData.set("enableU2f", checked)
}
@@ -240,7 +195,7 @@ Item {
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
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")
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")]
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 => {
@@ -290,7 +245,7 @@ Item {
StyledText {
text: I18n.tr("Path to a video file or folder containing videos")
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeXSmall
color: Theme.outlineVariant
wrapMode: Text.WordWrap
width: parent.width

View File

@@ -1935,6 +1935,11 @@ Item {
label: I18n.tr("Auth Type"),
value: data["connection-type"]
});
fields.push({
label: I18n.tr("Autoconnect"),
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
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 {
width: 1
height: Theme.spacingXS

View File

@@ -20,10 +20,7 @@ Item {
var out = [];
for (var i = 0; i < rules.length; i++) {
if ((rules[i].action || "").toString().toLowerCase() === "mute")
out.push({
rule: rules[i],
index: i
});
out.push({ rule: rules[i], index: i });
}
return out;
}
@@ -291,15 +288,6 @@ Item {
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 {
width: parent.width
height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2
@@ -352,7 +340,6 @@ Item {
}
SettingsSliderRow {
id: animationDurationSlider
settingKey: "notificationCustomAnimationDuration"
tags: ["notification", "animation", "duration", "custom", "speed"]
text: I18n.tr("Duration")
@@ -368,13 +355,6 @@ Item {
}
SettingsData.set("notificationCustomAnimationDuration", newValue);
}
Connections {
target: Theme
function onNotificationAnimationBaseDurationChanged() {
animationDurationSlider.value = Theme.notificationAnimationBaseDuration;
}
}
}
}
}

View File

@@ -408,8 +408,6 @@ FloatingWindow {
}
clip: true
visible: !root.isLoading
add: null
displaced: null
ScrollBar.vertical: DankScrollbar {
id: browserScrollbar

View File

@@ -2,6 +2,7 @@ import QtCore
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
@@ -750,6 +751,234 @@ Item {
}
}
Column {
id: variantSelector
width: parent.width
spacing: Theme.spacingS
visible: activeThemeId !== "" && activeThemeVariants !== null && (isMultiVariant || (activeThemeVariants.options && activeThemeVariants.options.length > 0))
property string activeThemeId: {
if (Theme.currentThemeCategory !== "registry" || Theme.currentTheme !== "custom")
return "";
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
return t.id;
}
return "";
}
property var activeThemeVariants: {
if (!activeThemeId)
return null;
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (t.id === activeThemeId && t.hasVariants)
return t.variants;
}
return null;
}
property bool isMultiVariant: activeThemeVariants?.type === "multi"
property string colorMode: Theme.isLightMode ? "light" : "dark"
property var multiDefaults: {
if (!isMultiVariant || !activeThemeVariants?.defaults)
return {};
return activeThemeVariants.defaults[colorMode] || activeThemeVariants.defaults.dark || {};
}
property var storedMulti: activeThemeId ? SettingsData.getRegistryThemeMultiVariant(activeThemeId, multiDefaults, colorMode) : multiDefaults
property string selectedFlavor: {
var sf = storedMulti.flavor || multiDefaults.flavor || "";
for (var i = 0; i < flavorOptions.length; i++) {
if (flavorOptions[i].id === sf)
return sf;
}
if (flavorOptions.length > 0)
return flavorOptions[0].id;
return sf;
}
property string selectedAccent: storedMulti.accent || multiDefaults.accent || ""
property var flavorOptions: {
if (!isMultiVariant || !activeThemeVariants?.flavors)
return [];
return activeThemeVariants.flavors.filter(f => {
if (f.mode)
return f.mode === colorMode || f.mode === "both";
return !!f[colorMode];
});
}
property var flavorNames: flavorOptions.map(f => f.name)
property int flavorIndex: {
for (var i = 0; i < flavorOptions.length; i++) {
if (flavorOptions[i].id === selectedFlavor)
return i;
}
return 0;
}
property string selectedVariant: activeThemeId ? SettingsData.getRegistryThemeVariant(activeThemeId, activeThemeVariants?.default || "") : ""
property var variantNames: {
if (!activeThemeVariants?.options)
return [];
return activeThemeVariants.options.map(v => v.name);
}
property int selectedIndex: {
if (!activeThemeVariants?.options || !selectedVariant)
return 0;
for (var i = 0; i < activeThemeVariants.options.length; i++) {
if (activeThemeVariants.options[i].id === selectedVariant)
return i;
}
return 0;
}
Item {
width: parent.width
height: flavorButtonGroup.implicitHeight
clip: true
visible: variantSelector.isMultiVariant && variantSelector.flavorOptions.length > 1
DankButtonGroup {
id: flavorButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
property int _count: variantSelector.flavorNames.length
property real _maxPerItem: _count > 1 ? (parent.width - (_count - 1) * spacing) / _count : parent.width
buttonPadding: _maxPerItem < 55 ? Theme.spacingXS : (_maxPerItem < 75 ? Theme.spacingS : Theme.spacingL)
minButtonWidth: Math.min(_maxPerItem < 55 ? 28 : (_maxPerItem < 75 ? 44 : 64), Math.max(28, Math.floor(_maxPerItem)))
textSize: _maxPerItem < 55 ? Theme.fontSizeSmall - 2 : (_maxPerItem < 75 ? Theme.fontSizeSmall : Theme.fontSizeMedium)
checkEnabled: _maxPerItem >= 55
property int pendingIndex: -1
model: variantSelector.flavorNames
currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.flavorIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingIndex = index;
}
onAnimationCompleted: {
if (pendingIndex < 0 || pendingIndex >= variantSelector.flavorOptions.length)
return;
const flavorId = variantSelector.flavorOptions[pendingIndex]?.id;
const idx = pendingIndex;
pendingIndex = -1;
if (!flavorId || flavorId === variantSelector.selectedFlavor)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, flavorId, variantSelector.selectedAccent, variantSelector.colorMode);
}
}
}
Item {
width: parent.width
height: accentColorsGrid.implicitHeight
visible: variantSelector.isMultiVariant && variantSelector.activeThemeVariants?.accents?.length > 0
Grid {
id: accentColorsGrid
property int accentCount: variantSelector.activeThemeVariants?.accents?.length ?? 0
property int dotSize: parent.width < 300 ? 28 : 32
columns: accentCount > 0 ? Math.ceil(accentCount / 2) : 1
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: variantSelector.activeThemeVariants?.accents || []
Rectangle {
required property var modelData
required property int index
property string accentId: modelData.id
property bool isSelected: accentId === variantSelector.selectedAccent
width: accentColorsGrid.dotSize
height: accentColorsGrid.dotSize
radius: width / 2
color: modelData.color || Theme.primary
border.color: Theme.outline
border.width: isSelected ? 2 : 1
scale: isSelected ? 1.1 : 1
Rectangle {
width: accentNameText.contentWidth + Theme.spacingS * 2
height: accentNameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: accentMouseArea.containsMouse
StyledText {
id: accentNameText
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: accentMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (parent.isSelected)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, parent.accentId, variantSelector.colorMode);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
Item {
width: parent.width
height: variantButtonGroup.implicitHeight
clip: true
visible: !variantSelector.isMultiVariant && variantSelector.variantNames.length > 0
DankButtonGroup {
id: variantButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
property int _count: variantSelector.variantNames.length
property real _maxPerItem: _count > 1 ? (parent.width - (_count - 1) * spacing) / _count : parent.width
buttonPadding: _maxPerItem < 55 ? Theme.spacingXS : (_maxPerItem < 75 ? Theme.spacingS : Theme.spacingL)
minButtonWidth: Math.min(_maxPerItem < 55 ? 28 : (_maxPerItem < 75 ? 44 : 64), Math.max(28, Math.floor(_maxPerItem)))
textSize: _maxPerItem < 55 ? Theme.fontSizeSmall - 2 : (_maxPerItem < 75 ? Theme.fontSizeSmall : Theme.fontSizeMedium)
checkEnabled: _maxPerItem >= 55
property int pendingIndex: -1
model: variantSelector.variantNames
currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.selectedIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingIndex = index;
}
onAnimationCompleted: {
if (pendingIndex < 0 || !variantSelector.activeThemeVariants?.options)
return;
const variantId = variantSelector.activeThemeVariants.options[pendingIndex]?.id;
const idx = pendingIndex;
pendingIndex = -1;
if (!variantId || variantId === variantSelector.selectedVariant)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeVariant(variantSelector.activeThemeId, variantId);
}
}
}
}
StyledText {
text: I18n.tr("No themes installed. Browse themes to install from the registry.", "no registry themes installed hint")
font.pixelSize: Theme.fontSizeSmall
@@ -767,248 +996,6 @@ Item {
onClicked: showThemeBrowser()
}
}
Column {
id: variantSelector
width: parent.width
spacing: Theme.spacingS
visible: activeThemeId !== "" && activeThemeVariants !== null && (isMultiVariant || (activeThemeVariants.options && activeThemeVariants.options.length > 0))
property string activeThemeId: {
switch (Theme.currentThemeCategory) {
case "registry":
if (Theme.currentTheme !== "custom")
return "";
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (SettingsData.customThemeFile && SettingsData.customThemeFile.endsWith((t.sourceDir || t.id) + "/theme.json"))
return t.id;
}
return "";
case "custom":
return Theme.currentThemeId || "";
default:
return "";
}
}
property var activeThemeVariants: {
if (!activeThemeId)
return null;
switch (Theme.currentThemeCategory) {
case "registry":
for (var i = 0; i < themeColorsTab.installedRegistryThemes.length; i++) {
var t = themeColorsTab.installedRegistryThemes[i];
if (t.id === activeThemeId && t.hasVariants)
return t.variants;
}
return null;
case "custom":
return Theme.currentThemeVariants || null;
default:
return null;
}
}
property bool isMultiVariant: activeThemeVariants?.type === "multi"
property string colorMode: Theme.isLightMode ? "light" : "dark"
property var multiDefaults: {
if (!isMultiVariant || !activeThemeVariants?.defaults)
return {};
return activeThemeVariants.defaults[colorMode] || activeThemeVariants.defaults.dark || {};
}
property var storedMulti: activeThemeId ? SettingsData.getRegistryThemeMultiVariant(activeThemeId, multiDefaults, colorMode) : multiDefaults
property string selectedFlavor: {
var sf = storedMulti.flavor || multiDefaults.flavor || "";
for (var i = 0; i < flavorOptions.length; i++) {
if (flavorOptions[i].id === sf)
return sf;
}
if (flavorOptions.length > 0)
return flavorOptions[0].id;
return sf;
}
property string selectedAccent: storedMulti.accent || multiDefaults.accent || ""
property var flavorOptions: {
if (!isMultiVariant || !activeThemeVariants?.flavors)
return [];
return activeThemeVariants.flavors.filter(f => {
if (f.mode)
return f.mode === colorMode || f.mode === "both";
return !!f[colorMode];
});
}
property var flavorNames: flavorOptions.map(f => f.name)
property int flavorIndex: {
for (var i = 0; i < flavorOptions.length; i++) {
if (flavorOptions[i].id === selectedFlavor)
return i;
}
return 0;
}
property string selectedVariant: activeThemeId ? SettingsData.getRegistryThemeVariant(activeThemeId, activeThemeVariants?.default || "") : ""
property var variantNames: {
if (!activeThemeVariants?.options)
return [];
return activeThemeVariants.options.map(v => v.name);
}
property int selectedIndex: {
if (!activeThemeVariants?.options || !selectedVariant)
return 0;
for (var i = 0; i < activeThemeVariants.options.length; i++) {
if (activeThemeVariants.options[i].id === selectedVariant)
return i;
}
return 0;
}
Item {
width: parent.width
height: flavorButtonGroup.implicitHeight
clip: true
visible: variantSelector.isMultiVariant && variantSelector.flavorOptions.length > 1
DankButtonGroup {
id: flavorButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
property int _count: variantSelector.flavorNames.length
property real _maxPerItem: _count > 1 ? (parent.width - (_count - 1) * spacing) / _count : parent.width
buttonPadding: _maxPerItem < 55 ? Theme.spacingXS : (_maxPerItem < 75 ? Theme.spacingS : Theme.spacingL)
minButtonWidth: Math.min(_maxPerItem < 55 ? 28 : (_maxPerItem < 75 ? 44 : 64), Math.max(28, Math.floor(_maxPerItem)))
textSize: _maxPerItem < 55 ? Theme.fontSizeSmall - 2 : (_maxPerItem < 75 ? Theme.fontSizeSmall : Theme.fontSizeMedium)
checkEnabled: _maxPerItem >= 55
property int pendingIndex: -1
model: variantSelector.flavorNames
currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.flavorIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingIndex = index;
}
onAnimationCompleted: {
if (pendingIndex < 0 || pendingIndex >= variantSelector.flavorOptions.length)
return;
const flavorId = variantSelector.flavorOptions[pendingIndex]?.id;
const idx = pendingIndex;
pendingIndex = -1;
if (!flavorId || flavorId === variantSelector.selectedFlavor)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, flavorId, variantSelector.selectedAccent, variantSelector.colorMode);
}
}
}
Item {
width: parent.width
height: accentColorsGrid.implicitHeight
visible: variantSelector.isMultiVariant && variantSelector.activeThemeVariants?.accents?.length > 0
Grid {
id: accentColorsGrid
property int accentCount: variantSelector.activeThemeVariants?.accents?.length ?? 0
property int dotSize: parent.width < 300 ? 28 : 32
columns: accentCount > 0 ? Math.ceil(accentCount / 2) : 1
rowSpacing: Theme.spacingS
columnSpacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: variantSelector.activeThemeVariants?.accents || []
Rectangle {
required property var modelData
required property int index
property string accentId: modelData.id
property bool isSelected: accentId === variantSelector.selectedAccent
width: accentColorsGrid.dotSize
height: accentColorsGrid.dotSize
radius: width / 2
color: modelData.color || modelData[variantSelector.selectedFlavor]?.primary || Theme.primary
border.color: Theme.outline
border.width: isSelected ? 2 : 1
scale: isSelected ? 1.1 : 1
Rectangle {
width: accentNameText.contentWidth + Theme.spacingS * 2
height: accentNameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: accentMouseArea.containsMouse
StyledText {
id: accentNameText
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: accentMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (parent.isSelected)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeMultiVariant(variantSelector.activeThemeId, variantSelector.selectedFlavor, parent.accentId, variantSelector.colorMode);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
Item {
width: parent.width
height: variantButtonGroup.implicitHeight
clip: true
visible: !variantSelector.isMultiVariant && variantSelector.variantNames.length > 0
DankButtonGroup {
id: variantButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
property int _count: variantSelector.variantNames.length
property real _maxPerItem: _count > 1 ? (parent.width - (_count - 1) * spacing) / _count : parent.width
buttonPadding: _maxPerItem < 55 ? Theme.spacingXS : (_maxPerItem < 75 ? Theme.spacingS : Theme.spacingL)
minButtonWidth: Math.min(_maxPerItem < 55 ? 28 : (_maxPerItem < 75 ? 44 : 64), Math.max(28, Math.floor(_maxPerItem)))
textSize: _maxPerItem < 55 ? Theme.fontSizeSmall - 2 : (_maxPerItem < 75 ? Theme.fontSizeSmall : Theme.fontSizeMedium)
checkEnabled: _maxPerItem >= 55
property int pendingIndex: -1
model: variantSelector.variantNames
currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.selectedIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
pendingIndex = index;
}
onAnimationCompleted: {
if (pendingIndex < 0 || !variantSelector.activeThemeVariants?.options)
return;
const variantId = variantSelector.activeThemeVariants.options[pendingIndex]?.id;
const idx = pendingIndex;
pendingIndex = -1;
if (!variantId || variantId === variantSelector.selectedVariant)
return;
Theme.screenTransition();
SettingsData.setRegistryThemeVariant(variantSelector.activeThemeId, variantId);
}
}
}
}
}
}
@@ -2568,96 +2555,18 @@ Item {
onToggled: checked => SettingsData.set("matugenTemplateFoot", checked)
}
SettingsDivider {
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
}
SettingsToggleRow {
id: neovimThemeToggle
tab: "theme"
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovim"
text: "neovim"
description: getTemplateDescription("nvim", I18n.tr("Required plugin: ") + "https://github.com/AvengeMedia/base46")
description: getTemplateDescription("nvim", I18n.tr("Requires lazy plugin manager", "neovim template description"))
descriptionColor: getTemplateDescriptionColor("nvim")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateNeovim
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
}
SettingsDropdownRow {
text: I18n.tr("Dark mode base")
tab: "theme"
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovimSettings"
description: "Base to derive dark theme from"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
currentValue: SettingsData.matugenTemplateNeovimSettings?.dark?.baseTheme ?? "github_dark"
options: ["aquarium", "ashes", "aylin", "ayu_dark", "bearded-arc", "carbonfox", "catppuccin", "chadracula", "chadracula-evondev", "chadtain", "chocolate", "darcula-dark", "dark_horizon", "decay", "default-dark", "doomchad", "eldritch", "embark", "everblush", "everforest", "falcon", "flexoki", "flouromachine", "gatekeeper", "github_dark", "gruvbox", "gruvchad", "hiberbee", "horizon", "jabuti", "jellybeans", "kanagawa", "kanagawa-dragon", "material-darker", "material-deep-ocean", "melange", "midnight_breeze", "mito-laser", "monekai", "monochrome", "mountain", "neofusion", "nightfox", "nightlamp", "nightowl", "nord", "obsidian-ember", "oceanic-next", "onedark", "onenord", "oxocarbon", "palenight", "pastelDark", "pastelbeans", "penumbra_dark", "poimandres", "radium", "rosepine", "rxyhn", "scaryforest", "seoul256_dark", "solarized_dark", "solarized_osaka", "starlight", "sweetpastel", "tokyodark", "tokyonight", "tomorrow_night", "tundra", "vesper", "vscode_dark", "wombat", "yoru", "zenburn"]
enableFuzzySearch: true
onValueChanged: value => {
const settings = SettingsData.matugenTemplateNeovimSettings;
settings.dark.baseTheme = value;
SettingsData.set("matugenTemplateNeovimSettings", settings);
}
}
SettingsDropdownRow {
text: I18n.tr("Light mode base")
tab: "theme"
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovimSettings"
description: "Base to derive light theme from"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
currentValue: SettingsData.matugenTemplateNeovimSettings?.light?.baseTheme ?? "github_light"
options: ["ayu_light", "blossom_light", "catppuccin-latte", "default-light", "everforest_light", "flex-light", "flexoki-light", "github_light", "gruvbox_light", "material-lighter", "nano-light", "oceanic-light", "one_light", "onenord_light", "penumbra_light", "rosepine-dawn", "seoul256_light", "solarized_light", "sunrise_breeze", "vscode_light"]
enableFuzzySearch: true
onValueChanged: value => {
const settings = SettingsData.matugenTemplateNeovimSettings;
settings.light.baseTheme = value;
SettingsData.set("matugenTemplateNeovimSettings", settings);
}
}
SettingsSliderRow {
text: I18n.tr("Dark mode harmony")
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovimSettings"
description: "How much should the base dark theme be tinted"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
minimum: 0
maximum: 100
value: (SettingsData.matugenTemplateNeovimSettings?.dark?.harmony ?? 0.5) * 100
defaultValue: 50
onSliderValueChanged: value => {
const settings = SettingsData.matugenTemplateNeovimSettings;
settings.dark.harmony = value / 100;
SettingsData.set("matugenTemplateNeovimSettings", settings);
}
}
SettingsSliderRow {
text: I18n.tr("Light mode harmony")
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovimSettings"
description: "How much should the base light theme be tinted"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
minimum: 0
maximum: 100
value: (SettingsData.matugenTemplateNeovimSettings?.light?.harmony ?? 0.5) * 100
defaultValue: 50
onSliderValueChanged: value => {
const settings = SettingsData.matugenTemplateNeovimSettings;
settings.light.harmony = value / 100;
SettingsData.set("matugenTemplateNeovimSettings", settings);
}
}
SettingsDivider {
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
}
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "alacritty", "terminal", "template"]
@@ -2729,18 +2638,6 @@ Item {
checked: SettingsData.matugenTemplateEmacs
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 {

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