From 32d16d067364e3181922bba5f5a5a4f2b0515a7b Mon Sep 17 00:00:00 2001 From: purian23 Date: Wed, 4 Mar 2026 14:17:56 -0500 Subject: [PATCH] refactor(greeter): Update auth flows and add configurable opts - Finally fix debug info logs before dms greeter loads - prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync --- core/cmd/dms/commands_greeter.go | 187 ++++----- core/cmd/dms/utils.go | 8 - core/internal/greeter/installer.go | 299 ++++++++++---- quickshell/Common/SettingsData.qml | 2 + quickshell/Common/settings/SettingsSpec.js | 2 + quickshell/Modules/Greetd/GreetdEnv.js | 20 + quickshell/Modules/Greetd/GreetdMemory.qml | 33 +- quickshell/Modules/Greetd/GreetdSettings.qml | 109 +++-- quickshell/Modules/Greetd/GreeterContent.qml | 375 ++++++++++++++++-- quickshell/Modules/Greetd/README.md | 2 + quickshell/Modules/Greetd/assets/dms-greeter | 122 +++++- quickshell/Modules/Lock/LockScreenContent.qml | 19 +- quickshell/Modules/Lock/Pam.qml | 76 +++- 13 files changed, 969 insertions(+), 285 deletions(-) create mode 100644 quickshell/Modules/Greetd/GreetdEnv.js diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index 6ebdbe0c..abcd9854 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -486,22 +486,22 @@ func enableGreeter() error { configPath := "/etc/greetd/config.toml" if _, err := os.Stat(configPath); os.IsNotExist(err) { return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath) + } else if err != nil { + return fmt.Errorf("failed to access greetd config at %s: %w", configPath, err) } - data, err := os.ReadFile(configPath) - if err != nil { - return fmt.Errorf("failed to read greetd config: %w", err) - } - - configContent := string(data) if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") } - configAlreadyCorrect := strings.Contains(configContent, "dms-greeter") + configAlreadyCorrect := isGreeterEnabled() + configuredCompositor := detectConfiguredCompositor() if configAlreadyCorrect { fmt.Println("✓ Greeter is already configured with dms-greeter") + if configuredCompositor != "" { + fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor) + } if err := ensureGraphicalTarget(); err != nil { return err @@ -548,68 +548,21 @@ func enableGreeter() error { fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor) } - backupPath := configPath + ".backup" - backupCmd := exec.Command("sudo", "cp", configPath, backupPath) - if err := backupCmd.Run(); err != nil { - return fmt.Errorf("failed to backup config: %w", err) - } - fmt.Printf("✓ Backed up config to %s\n", backupPath) - - lines := strings.Split(configContent, "\n") - var newLines []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { - newLines = append(newLines, line) + greeterPathForConfig := "" + if !greeter.IsGreeterPackaged() { + dmsPath, err := greeter.DetectDMSPath() + if err != nil { + return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err) } + greeterPathForConfig = dmsPath } - - wrapperCmd, err := findCommandPath("dms-greeter") - if err != nil { - return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible") + logFunc := func(msg string) { + fmt.Println(msg) } - - compositorLower := strings.ToLower(selectedCompositor) - commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower) - - var finalLines []string - inDefaultSession := false - commandAdded := false - - for _, line := range newLines { - finalLines = append(finalLines, line) - trimmed := strings.TrimSpace(line) - - if trimmed == "[default_session]" { - inDefaultSession = true - } - - if inDefaultSession && !commandAdded { - if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { - finalLines = append(finalLines, commandLine) - commandAdded = true - } - } + if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil { + return fmt.Errorf("failed to configure greetd: %w", err) } - if !commandAdded { - finalLines = append(finalLines, commandLine) - } - - newConfig := strings.Join(finalLines, "\n") - - tmpFile := "/tmp/greetd-config.toml" - if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil { - return fmt.Errorf("failed to write temp config: %w", err) - } - - moveCmd := exec.Command("sudo", "mv", tmpFile, configPath) - if err := moveCmd.Run(); err != nil { - return fmt.Errorf("failed to update config: %w", err) - } - - fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor) - if err := ensureGraphicalTarget(); err != nil { return err } @@ -631,32 +584,69 @@ func enableGreeter() error { } func isGreeterEnabled() bool { - data, err := os.ReadFile("/etc/greetd/config.toml") - if err != nil { - return false - } - return strings.Contains(string(data), "dms-greeter") + command := readDefaultSessionCommand("/etc/greetd/config.toml") + return command != "" && strings.Contains(command, "dms-greeter") } func detectConfiguredCompositor() string { - data, err := os.ReadFile("/etc/greetd/config.toml") + command := strings.ToLower(readDefaultSessionCommand("/etc/greetd/config.toml")) + switch { + case strings.Contains(command, "--command niri"): + return "niri" + case strings.Contains(command, "--command hyprland"): + return "hyprland" + case strings.Contains(command, "--command sway"): + return "sway" + } + return "" +} + +func stripTomlComment(line string) string { + trimmed := strings.TrimSpace(line) + if idx := strings.Index(trimmed, "#"); idx >= 0 { + return strings.TrimSpace(trimmed[:idx]) + } + return trimmed +} + +func parseTomlSection(line string) (string, bool) { + trimmed := stripTomlComment(line) + if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") { + return "", false + } + return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true +} + +func readDefaultSessionCommand(configPath string) string { + data, err := os.ReadFile(configPath) if err != nil { return "" } - for _, line := range strings.Split(string(data), "\n") { - trimmed := strings.TrimSpace(line) - if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") { + inDefaultSession := false + for line := range strings.SplitSeq(string(data), "\n") { + if section, ok := parseTomlSection(line); ok { + inDefaultSession = section == "default_session" continue } - switch { - case strings.Contains(trimmed, "--command niri"): - return "niri" - case strings.Contains(trimmed, "--command hyprland"): - return "hyprland" - case strings.Contains(trimmed, "--command sway"): - return "sway" + if !inDefaultSession { + continue + } + + trimmed := stripTomlComment(line) + if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { + continue + } + + parts := strings.SplitN(trimmed, "=", 2) + if len(parts) != 2 { + continue + } + + command := strings.Trim(strings.TrimSpace(parts[1]), `"`) + if command != "" { + return command } } @@ -742,30 +732,21 @@ func checkGreeterStatus() error { configPath := "/etc/greetd/config.toml" fmt.Println("Greeter Configuration:") - if data, err := os.ReadFile(configPath); err == nil { - configContent := string(data) - if strings.Contains(configContent, "dms-greeter") { - lines := strings.SplitSeq(configContent, "\n") - for line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") { - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) == 2 { - command := strings.Trim(strings.TrimSpace(parts[1]), `"`) - fmt.Println(" ✓ Greeter is enabled") + if _, err := os.ReadFile(configPath); err == nil { + command := readDefaultSessionCommand(configPath) + if command != "" && strings.Contains(command, "dms-greeter") { + fmt.Println(" ✓ Greeter is enabled") - if strings.Contains(command, "--command niri") { - fmt.Println(" Compositor: niri") - } else if strings.Contains(command, "--command hyprland") { - fmt.Println(" Compositor: Hyprland") - } else if strings.Contains(command, "--command sway") { - fmt.Println(" Compositor: sway") - } else { - fmt.Println(" Compositor: unknown") - } - } - break - } + compositor := detectConfiguredCompositor() + switch compositor { + case "niri": + fmt.Println(" Compositor: niri") + case "hyprland": + fmt.Println(" Compositor: Hyprland") + case "sway": + fmt.Println(" Compositor: sway") + default: + fmt.Println(" Compositor: unknown") } } else { fmt.Println(" ✗ Greeter is NOT enabled") diff --git a/core/cmd/dms/utils.go b/core/cmd/dms/utils.go index 9a2163c0..d5b3b046 100644 --- a/core/cmd/dms/utils.go +++ b/core/cmd/dms/utils.go @@ -7,14 +7,6 @@ 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() diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index 098a1660..f1e79d26 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -36,6 +36,184 @@ func DetectGreeterGroup() string { return "greeter" } +func hasPasswdUser(passwdData, user string) bool { + prefix := user + ":" + for line := range strings.SplitSeq(passwdData, "\n") { + if strings.HasPrefix(line, prefix) { + return true + } + } + return false +} + +func findPasswdUser(passwdData string, candidates ...string) (string, bool) { + for _, candidate := range candidates { + if hasPasswdUser(passwdData, candidate) { + return candidate, true + } + } + return "", false +} + +func stripTomlComment(line string) string { + trimmed := strings.TrimSpace(line) + if idx := strings.Index(trimmed, "#"); idx >= 0 { + return strings.TrimSpace(trimmed[:idx]) + } + return trimmed +} + +func parseTomlSection(line string) (string, bool) { + trimmed := stripTomlComment(line) + if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") { + return "", false + } + return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true +} + +func extractDefaultSessionUser(configContent string) string { + inDefaultSession := false + for line := range strings.SplitSeq(configContent, "\n") { + if section, ok := parseTomlSection(line); ok { + inDefaultSession = section == "default_session" + continue + } + + if !inDefaultSession { + continue + } + + trimmed := stripTomlComment(line) + if !strings.HasPrefix(trimmed, "user =") && !strings.HasPrefix(trimmed, "user=") { + continue + } + + parts := strings.SplitN(trimmed, "=", 2) + if len(parts) != 2 { + continue + } + user := strings.Trim(strings.TrimSpace(parts[1]), `"`) + if user != "" { + return user + } + } + + return "" +} + +func upsertDefaultSession(configContent, greeterUser, command string) string { + lines := strings.Split(configContent, "\n") + var out []string + + inDefaultSession := false + foundDefaultSession := false + defaultSessionUserSet := false + defaultSessionCommandSet := false + + appendDefaultSessionFields := func() { + if !defaultSessionUserSet { + out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser)) + } + if !defaultSessionCommandSet { + out = append(out, command) + } + } + + for _, line := range lines { + if section, ok := parseTomlSection(line); ok { + if inDefaultSession { + appendDefaultSessionFields() + } + + inDefaultSession = section == "default_session" + if inDefaultSession { + foundDefaultSession = true + defaultSessionUserSet = false + defaultSessionCommandSet = false + } + + out = append(out, line) + continue + } + + if inDefaultSession { + trimmed := stripTomlComment(line) + if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { + out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser)) + defaultSessionUserSet = true + continue + } + + if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") { + if !defaultSessionCommandSet { + out = append(out, command) + defaultSessionCommandSet = true + } + continue + } + } + + out = append(out, line) + } + + if inDefaultSession { + appendDefaultSessionFields() + } + + if !foundDefaultSession { + if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" { + out = append(out, "") + } + out = append(out, "[default_session]") + out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser)) + out = append(out, command) + } + + return strings.Join(out, "\n") +} + +func DetectGreeterUser() string { + passwdData, err := os.ReadFile("/etc/passwd") + if err == nil { + passwdContent := string(passwdData) + + if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil { + if configured := extractDefaultSessionUser(string(configData)); configured != "" && hasPasswdUser(passwdContent, configured) { + return configured + } + } + + if user, found := findPasswdUser(passwdContent, "greeter", "_greeter", "greetd"); found { + return user + } + } else { + fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/passwd, defaulting greeter user to 'greeter'") + } + + if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil { + if configured := extractDefaultSessionUser(string(configData)); configured != "" { + return configured + } + } + + fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter user found, defaulting to 'greeter'") + return "greeter" +} + +func resolveGreeterWrapperPath() string { + if path, err := exec.LookPath("dms-greeter"); err == nil { + return path + } + + for _, candidate := range []string{"/usr/local/bin/dms-greeter", "/usr/bin/dms-greeter"} { + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + + return "dms-greeter" +} + func DetectCompositors() []string { var compositors []string @@ -289,9 +467,13 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { - if utils.CommandExists("dms-greeter") { - logFunc("✓ dms-greeter wrapper already installed") + if IsGreeterPackaged() { + logFunc("✓ dms-greeter package already installed") } else { + if dmsPath == "" { + return fmt.Errorf("dms path is required for manual dms-greeter wrapper installs") + } + assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets") wrapperSrc := filepath.Join(assetsDir, "dms-greeter") @@ -300,10 +482,14 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass } wrapperDst := "/usr/local/bin/dms-greeter" + action := "Installed" + if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() { + action = "Updated" + } if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil { return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err) } - logFunc(fmt.Sprintf("✓ Installed dms-greeter wrapper to %s", wrapperDst)) + logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst)) if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil { return fmt.Errorf("failed to make wrapper executable: %w", err) @@ -954,90 +1140,59 @@ func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassw return fmt.Errorf("failed to backup config: %w", err) } logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath)) + } else if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to access %s: %w", configPath, err) } - greeterUser := DetectGreeterGroup() + greeterUser := DetectGreeterUser() var configContent string if data, err := os.ReadFile(configPath); err == nil { configContent = string(data) - } else { - configContent = fmt.Sprintf(`[terminal] + } else if os.IsNotExist(err) { + configContent = `[terminal] vt = 1 [default_session] - -user = "%s" -`, greeterUser) +` + } else { + return fmt.Errorf("failed to read greetd config: %w", err) } - lines := strings.Split(configContent, "\n") - var newLines []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { - if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { - newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser)) - } else { - newLines = append(newLines, line) - } - } - } - - // If dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter - wrapperCmd := "dms-greeter" - if !utils.CommandExists("dms-greeter") { - wrapperCmd = "/usr/local/bin/dms-greeter" - } + wrapperCmd := resolveGreeterWrapperPath() compositorLower := strings.ToLower(compositor) - var command string - if dmsPath == "" { - command = fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower) - } else { - command = fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath) - } - - var finalLines []string - inDefaultSession := false - commandAdded := false - - for _, line := range newLines { - finalLines = append(finalLines, line) - trimmed := strings.TrimSpace(line) - - if trimmed == "[default_session]" { - inDefaultSession = true - } - - if inDefaultSession && !commandAdded && trimmed != "" && !strings.HasPrefix(trimmed, "[") { - if !strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "user") { - finalLines = append(finalLines, command) - commandAdded = true - } - } - } - - if !commandAdded { - finalLines = append(finalLines, command) - } - - newConfig := strings.Join(finalLines, "\n") - - tmpFile := "/tmp/greetd-config.toml" - if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil { - return fmt.Errorf("failed to write temp config: %w", err) - } - - if err := runSudoCmd(sudoPassword, "mv", tmpFile, configPath); err != nil { - return fmt.Errorf("failed to move config to /etc/greetd: %w", err) - } - - cmdDesc := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower) + commandValue := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower) if dmsPath != "" { - cmdDesc = fmt.Sprintf("%s -p %s", cmdDesc, dmsPath) + commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath) } - logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, cmdDesc)) + + commandLine := fmt.Sprintf(`command = "%s"`, commandValue) + newConfig := upsertDefaultSession(configContent, greeterUser, commandLine) + + tmpFile, err := os.CreateTemp("", "greetd-config-*.toml") + if err != nil { + return fmt.Errorf("failed to create temp greetd config: %w", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(newConfig); err != nil { + _ = tmpFile.Close() + return fmt.Errorf("failed to write temp greetd config: %w", err) + } + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("failed to close temp greetd config: %w", err) + } + + if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil { + return fmt.Errorf("failed to create /etc/greetd: %w", err) + } + + if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil { + return fmt.Errorf("failed to install greetd config: %w", err) + } + + logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue)) return nil } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index be24edee..afe194c5 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -313,6 +313,8 @@ Singleton { property string centeringMode: "index" property string clockDateFormat: "" property string lockDateFormat: "" + property bool greeterRememberLastSession: true + property bool greeterRememberLastUser: true property int mediaSize: 1 property string appLauncherViewMode: "list" diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 90b0bbaa..4b40ce23 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -164,6 +164,8 @@ var SPEC = { centeringMode: { def: "index" }, clockDateFormat: { def: "" }, lockDateFormat: { def: "" }, + greeterRememberLastSession: { def: true }, + greeterRememberLastUser: { def: true }, mediaSize: { def: 1 }, appLauncherViewMode: { def: "list" }, diff --git a/quickshell/Modules/Greetd/GreetdEnv.js b/quickshell/Modules/Greetd/GreetdEnv.js new file mode 100644 index 00000000..fcce181e --- /dev/null +++ b/quickshell/Modules/Greetd/GreetdEnv.js @@ -0,0 +1,20 @@ +.pragma library + +function readBoolOverride(envReader, names, fallbackValue) { + for (let i = 0; i < names.length; i++) { + const name = names[i]; + const raw = envReader(name); + if (raw === undefined || raw === null || raw === "") + continue; + + const normalized = String(raw).trim().toLowerCase(); + if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") + return true; + if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") + return false; + + console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback"); + } + + return fallbackValue; +} diff --git a/quickshell/Modules/Greetd/GreetdMemory.qml b/quickshell/Modules/Greetd/GreetdMemory.qml index 48006761..dea72a64 100644 --- a/quickshell/Modules/Greetd/GreetdMemory.qml +++ b/quickshell/Modules/Greetd/GreetdMemory.qml @@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io +import "GreetdEnv.js" as GreetdEnv Singleton { id: root @@ -11,6 +12,8 @@ Singleton { 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 + "/memory.json" + readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true) + readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true) property string lastSessionId: "" property string lastSuccessfulUser: "" @@ -49,26 +52,44 @@ Singleton { if (!content || !content.trim()) return; const memory = JSON.parse(content); - lastSessionId = memory.lastSessionId || ""; - lastSuccessfulUser = memory.lastSuccessfulUser || ""; + lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : ""; + lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : ""; + if (!rememberLastSession || !rememberLastUser) + saveMemory(); } catch (e) { console.warn("Failed to parse greetd memory:", e); } } function saveMemory() { - memoryFileView.setText(JSON.stringify({ - "lastSessionId": lastSessionId, - "lastSuccessfulUser": lastSuccessfulUser - }, null, 2)); + let memory = {}; + if (rememberLastSession && lastSessionId) + memory.lastSessionId = lastSessionId; + if (rememberLastUser && lastSuccessfulUser) + memory.lastSuccessfulUser = lastSuccessfulUser; + memoryFileView.setText(JSON.stringify(memory, 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(); } diff --git a/quickshell/Modules/Greetd/GreetdSettings.qml b/quickshell/Modules/Greetd/GreetdSettings.qml index 79688a2d..98b9a921 100644 --- a/quickshell/Modules/Greetd/GreetdSettings.qml +++ b/quickshell/Modules/Greetd/GreetdSettings.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Common +import "GreetdEnv.js" as GreetdEnv Singleton { id: root @@ -41,6 +42,8 @@ Singleton { property string lockDateFormat: "" property bool lockScreenShowPowerActions: true property bool lockScreenShowProfileImage: true + property bool rememberLastSession: true + property bool rememberLastUser: true property bool powerActionConfirm: true property real powerActionHoldDuration: 0.5 property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] @@ -52,53 +55,73 @@ Singleton { function parseSettings(content) { try { + let settings = {}; if (content && content.trim()) { - 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; + settings = JSON.parse(content); + } - if (typeof Theme !== "undefined") { - if (currentThemeName === "custom" && customThemeFile) { - Theme.loadCustomThemeFromFile(customThemeFile); - } - Theme.applyGreeterTheme(currentThemeName); + 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; + 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; + } + 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); } + Theme.applyGreeterTheme(currentThemeName); } } catch (e) { console.warn("Failed to parse greetd settings:", e); + } finally { + settingsLoaded = true; } } @@ -133,5 +156,9 @@ Singleton { onLoaded: { parseSettings(settingsFile.text()); } + onLoadFailed: error => { + console.warn("Failed to load greetd settings:", error); + root.parseSettings(""); + } } } diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index 6fa57c8a..35e65b29 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -31,6 +31,17 @@ Item { signal launchRequested property bool weatherInitialized: false + property bool awaitingExternalAuth: false + property int defaultAuthTimeoutMs: 12000 + property int externalAuthTimeoutMs: 45000 + 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 faillockConfigText: "" function initWeatherService() { if (weatherInitialized) @@ -44,16 +55,238 @@ 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 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]; + 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 = ""; + } + 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(); + } + onLoadFailed: { + root.greetdPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + + FileView { + id: systemAuthPamWatcher + path: "/etc/pam.d/system-auth" + printErrors: false + onLoaded: { + root.systemAuthPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + } + onLoadFailed: { + root.systemAuthPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + + FileView { + id: commonAuthPamWatcher + path: "/etc/pam.d/common-auth" + printErrors: false + onLoaded: { + root.commonAuthPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + } + onLoadFailed: { + root.commonAuthPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + + FileView { + id: passwordAuthPamWatcher + path: "/etc/pam.d/password-auth" + printErrors: false + onLoaded: { + root.passwordAuthPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + } + onLoadFailed: { + root.passwordAuthPamText = ""; + 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(); @@ -63,6 +296,8 @@ Item { } function applyLastSuccessfulUser() { + if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser) + return; const lastUser = GreetdMemory.lastSuccessfulUser; if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { GreeterState.username = lastUser; @@ -72,6 +307,38 @@ Item { } } + function submitUsername(rawValue) { + const user = (rawValue || "").trim(); + if (!user) + return; + if (GreeterState.username !== user) { + passwordFailureCount = 0; + clearAuthFeedback(); + } + GreeterState.username = user; + GreeterState.showPasswordInput = true; + PortalService.getGreeterUserProfileImage(user); + GreeterState.passwordBuffer = ""; + } + + function startAuthSession() { + if (!GreeterState.showPasswordInput || !GreeterState.username) + return; + if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive) + return; + if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0) + return; + awaitingExternalAuth = false; + authTimeout.interval = defaultAuthTimeoutMs; + authTimeout.restart(); + Greetd.createSession(GreeterState.username); + } + + function isExternalAuthPrompt(message, responseRequired) { + // Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch). + return !responseRequired; + } + Component.onDestruction: { if (weatherInitialized) WeatherService.removeRef(); @@ -421,15 +688,10 @@ Item { } onAccepted: { if (GreeterState.showPasswordInput) { - if (Greetd.state === GreetdState.Inactive && GreeterState.username) { - Greetd.createSession(GreeterState.username); - } + root.startAuthSession(); } else { if (text.trim()) { - GreeterState.username = text.trim(); - GreeterState.showPasswordInput = true; - PortalService.getGreeterUserProfileImage(GreeterState.username); - GreeterState.passwordBuffer = ""; + root.submitUsername(text); syncingFromState = true; text = ""; syncingFromState = false; @@ -568,15 +830,10 @@ Item { enabled: true onClicked: { if (GreeterState.showPasswordInput) { - if (GreeterState.username) { - Greetd.createSession(GreeterState.username); - } + root.startAuthSession(); } else { if (inputField.text.trim()) { - GreeterState.username = inputField.text.trim(); - GreeterState.showPasswordInput = true; - PortalService.getGreeterUserProfileImage(GreeterState.username); - GreeterState.passwordBuffer = ""; + root.submitUsername(inputField.text); inputField.text = ""; } } @@ -601,20 +858,16 @@ Item { StyledText { Layout.fillWidth: true - Layout.preferredHeight: 20 + Layout.preferredHeight: 38 Layout.topMargin: -Theme.spacingS Layout.bottomMargin: -Theme.spacingS - text: { - if (GreeterState.pamState === "error") - return "Authentication error - try again"; - if (GreeterState.pamState === "fail") - return "Incorrect password"; - return ""; - } + text: root.authFeedbackMessage color: Theme.error font.pixelSize: Theme.fontSizeSmall horizontalAlignment: Text.AlignHCenter - opacity: GreeterState.pamState !== "" ? 1 : 0 + wrapMode: Text.WordWrap + maximumLineCount: 2 + opacity: root.authFeedbackMessage !== "" ? 1 : 0 Behavior on opacity { NumberAnimation { @@ -1029,9 +1282,11 @@ Item { return; if (!GreetdMemory.memoryReady) return; + if (!GreetdSettings.settingsLoaded) + return; - const savedSession = GreetdMemory.lastSessionId; - if (savedSession) { + const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : ""; + if (savedSession && GreetdSettings.rememberLastSession) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) { if (GreeterState.sessionPaths[i] === savedSession) { GreeterState.currentSessionIndex = i; @@ -1163,45 +1418,100 @@ Item { enabled: isPrimaryScreen function onAuthMessage(message, error, responseRequired, echoResponse) { + awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired); + authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs; + authTimeout.restart(); if (responseRequired) { Greetd.respond(GreeterState.passwordBuffer); GreeterState.passwordBuffer = ""; inputField.text = ""; return; } - if (!error) - Greetd.respond(""); + Greetd.respond(""); + } + + function onStateChanged() { + if (Greetd.state === GreetdState.Inactive) { + awaitingExternalAuth = false; + authTimeout.interval = defaultAuthTimeoutMs; + authTimeout.stop(); + } } function onReadyToLaunch() { + awaitingExternalAuth = false; + 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(); - GreetdMemory.setLastSessionId(sessionPath); - GreetdMemory.setLastSuccessfulUser(GreeterState.username); + 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(""); + } Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]); } function onAuthFailure(message) { + awaitingExternalAuth = false; + authTimeout.interval = defaultAuthTimeoutMs; + authTimeout.stop(); launchTimeout.stop(); GreeterState.unlocking = false; - GreeterState.pamState = "fail"; + if (isLikelyLockoutMessage(message)) { + GreeterState.pamState = "max"; + } else { + GreeterState.pamState = "fail"; + passwordFailureCount = passwordFailureCount + 1; + } + authFeedbackMessage = currentAuthMessage(); GreeterState.passwordBuffer = ""; inputField.text = ""; placeholderDelay.restart(); + Greetd.cancelSession(); } function onError(error) { + awaitingExternalAuth = false; + 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: authTimeout + interval: defaultAuthTimeoutMs + onTriggered: { + if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive) + return; + awaitingExternalAuth = false; + authTimeout.interval = defaultAuthTimeoutMs; + GreeterState.pamState = "error"; + authFeedbackMessage = currentAuthMessage(); GreeterState.passwordBuffer = ""; inputField.text = ""; placeholderDelay.restart(); @@ -1217,6 +1527,7 @@ Item { return; GreeterState.unlocking = false; GreeterState.pamState = "error"; + authFeedbackMessage = currentAuthMessage(); placeholderDelay.restart(); Greetd.cancelSession(); } @@ -1225,7 +1536,7 @@ Item { Timer { id: placeholderDelay interval: 4000 - onTriggered: GreeterState.pamState = "" + onTriggered: clearAuthFeedback() } LockPowerMenu { diff --git a/quickshell/Modules/Greetd/README.md b/quickshell/Modules/Greetd/README.md index 762ca587..1975609f 100644 --- a/quickshell/Modules/Greetd/README.md +++ b/quickshell/Modules/Greetd/README.md @@ -9,6 +9,7 @@ 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 @@ -212,6 +213,7 @@ 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`: diff --git a/quickshell/Modules/Greetd/assets/dms-greeter b/quickshell/Modules/Greetd/assets/dms-greeter index 4b1597fc..dfaf6695 100755 --- a/quickshell/Modules/Greetd/assets/dms-greeter +++ b/quickshell/Modules/Greetd/assets/dms-greeter @@ -6,6 +6,9 @@ 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 @@ -22,6 +25,15 @@ 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: @@ -30,6 +42,7 @@ 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 @@ -43,6 +56,41 @@ 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) @@ -61,6 +109,26 @@ 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 @@ -113,8 +181,38 @@ export EGL_PLATFORM=gbm export DMS_RUN_GREETER=1 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 + mkdir -p "$CACHE_DIR" +# Keep greeter VT clean by default; callers can override via env or --debug. +if [[ -z "${RUST_LOG:-}" ]]; then + export RUST_LOG=warn +fi +if [[ -z "${NIRI_LOG:-}" ]]; then + export NIRI_LOG=warn +fi + if command -v qs >/dev/null 2>&1; then QS_BIN="qs" elif command -v quickshell >/dev/null 2>&1; then @@ -130,7 +228,9 @@ if [[ "$DMS_PATH" == /* ]]; then else RESOLVED_PATH=$(locate_dms_config "$DMS_PATH") if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then - echo "Located DMS config at: $RESOLVED_PATH" >&2 + if [[ "$DEBUG_MODE" == "1" ]]; then + echo "Located DMS config at: $RESOLVED_PATH" >&2 + fi 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 @@ -192,7 +292,7 @@ NIRI_EOF spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation" NIRI_EOF COMPOSITOR_CONFIG="$TEMP_CONFIG" - exec niri -c "$COMPOSITOR_CONFIG" + exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG" ;; hyprland) @@ -222,9 +322,9 @@ HYPRLAND_EOF COMPOSITOR_CONFIG="$TEMP_CONFIG" fi if command -v start-hyprland >/dev/null 2>&1; then - exec start-hyprland -- --config "$COMPOSITOR_CONFIG" + exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG" else - exec Hyprland -c "$COMPOSITOR_CONFIG" + exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG" fi ;; @@ -245,7 +345,7 @@ exec "$QS_CMD; swaymsg exit" SWAY_EOF COMPOSITOR_CONFIG="$TEMP_CONFIG" fi - exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG" + exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG" ;; scroll) @@ -265,7 +365,7 @@ exec "$QS_CMD; scrollmsg exit" SCROLL_EOF COMPOSITOR_CONFIG="$TEMP_CONFIG" fi - exec scroll -c "$COMPOSITOR_CONFIG" + exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG" ;; miracle|miracle-wm) @@ -285,24 +385,24 @@ exec "$QS_CMD; miraclemsg exit" MIRACLE_EOF COMPOSITOR_CONFIG="$TEMP_CONFIG" fi - exec miracle-wm -c "$COMPOSITOR_CONFIG" + exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG" ;; labwc) require_command "labwc" if [[ -n "$COMPOSITOR_CONFIG" ]]; then - exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD" + exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD" else - exec labwc --session "$QS_CMD" + exec_compositor "labwc" labwc --session "$QS_CMD" fi ;; mango|mangowc) require_command "mango" if [[ -n "$COMPOSITOR_CONFIG" ]]; then - exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" + exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" else - exec mango -s "$QS_CMD && mmsg -d quit" + exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit" fi ;; diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index 2072f16d..769c4e80 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -755,7 +755,7 @@ Item { } } onAccepted: { - if (!demoMode && !pam.passwd.active && !pam.u2fPending) { + if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) { pam.passwd.start(); } } @@ -764,6 +764,11 @@ Item { return; } + if (root.unlocking) { + event.accepted = true; + return; + } + if (event.key === Qt.Key_Escape) { if (pam.u2fPending) { pam.cancelU2fPending(); @@ -1017,7 +1022,7 @@ Item { visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending)) enabled: !demoMode onClicked: { - if (!demoMode && !pam.u2fPending) { + if (!demoMode && !root.unlocking && !pam.u2fPending) { pam.passwd.start(); } } @@ -1626,6 +1631,7 @@ Item { onStateChanged: { root.pamState = state; if (state !== "") { + root.unlocking = false; placeholderDelay.restart(); passwordField.text = ""; root.passwordBuffer = ""; @@ -1641,6 +1647,15 @@ Item { } } + Connections { + target: pam + + function onUnlockInProgressChanged() { + if (!pam.unlockInProgress && root.unlocking) + root.unlocking = false; + } + } + Binding { target: pam property: "buffer" diff --git a/quickshell/Modules/Lock/Pam.qml b/quickshell/Modules/Lock/Pam.qml index 28346e09..9fd318a6 100644 --- a/quickshell/Modules/Lock/Pam.qml +++ b/quickshell/Modules/Lock/Pam.qml @@ -25,6 +25,29 @@ 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; @@ -36,6 +59,7 @@ Scope { u2fPendingTimeout.running = false; u2fPending = false; u2fState = ""; + unlockRequestTimeout.restart(); unlockRequested(); } } @@ -102,6 +126,13 @@ 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) @@ -114,6 +145,18 @@ Scope { } } + Connections { + target: passwd + + function onActiveChanged() { + if (passwd.active) { + passwdActiveTimeout.restart(); + } else { + passwdActiveTimeout.running = false; + } + } + } + PamContext { id: fprint @@ -279,6 +322,26 @@ 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 @@ -308,17 +371,9 @@ Scope { root.u2fState = ""; root.u2fPending = false; root.lockMessage = ""; - root.unlockInProgress = false; + root.resetAuthFlows(); } else { - fprint.abort(); - passwd.abort(); - u2f.abort(); - errorRetry.running = false; - u2fErrorRetry.running = false; - u2fPendingTimeout.running = false; - root.u2fPending = false; - root.u2fState = ""; - root.unlockInProgress = false; + root.resetAuthFlows(); } } @@ -338,6 +393,7 @@ Scope { u2f.abort(); u2fErrorRetry.running = false; u2fPendingTimeout.running = false; + unlockRequestTimeout.running = false; root.u2fPending = false; root.u2fState = ""; u2f.checkAvail();