1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -04:00

feat(Greeter): add auto-login feature for startup settings

- Introduced a new cli flag:
`dms greeter sync --autologin-only`
and updated UI toggle in Greeter settings
This commit is contained in:
purian23
2026-06-02 02:03:02 -04:00
parent 8c20f448ed
commit 335c5b4ac5
12 changed files with 1106 additions and 156 deletions
+64 -4
View File
@@ -3,6 +3,7 @@ package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
@@ -74,13 +75,20 @@ var greeterSyncCmd = &cobra.Command{
auth, _ := cmd.Flags().GetBool("auth")
local, _ := cmd.Flags().GetBool("local")
profile, _ := cmd.Flags().GetBool("profile")
autologinOnly, _ := cmd.Flags().GetBool("autologin-only")
term, _ := cmd.Flags().GetBool("terminal")
if term {
if err := syncInTerminal(yes, auth, local, profile); err != nil {
if err := syncInTerminal(yes, auth, local, profile, autologinOnly); err != nil {
log.Fatalf("Error launching sync in terminal: %v", err)
}
return
}
if autologinOnly {
if err := syncGreeterAutoLoginOnly(yes); err != nil {
log.Fatalf("Error syncing greeter auto-login: %v", err)
}
return
}
if err := syncGreeter(yes, auth, local, profile); err != nil {
log.Fatalf("Error syncing greeter: %v", err)
}
@@ -93,6 +101,7 @@ func init() {
greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles")
greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path")
greeterSyncCmd.Flags().BoolP("profile", "p", false, "Sync only your per-user greeter slot (no sudo; for secondary accounts)")
greeterSyncCmd.Flags().Bool("autologin-only", false, "Apply only greeter auto-login on startup settings to greetd (no theme or auth sync)")
}
var greeterEnableCmd = &cobra.Command{
@@ -520,8 +529,8 @@ func runCommandInTerminal(shellCmd string) error {
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error {
syncFlags := make([]string, 0, 4)
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly bool, autologinOnly bool) error {
syncFlags := make([]string, 0, 5)
if nonInteractive {
syncFlags = append(syncFlags, "--yes")
}
@@ -534,11 +543,19 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly
if profileOnly {
syncFlags = append(syncFlags, "--profile")
}
if autologinOnly {
syncFlags = append(syncFlags, "--autologin-only")
}
shellSyncCmd := "dms greeter sync"
if len(syncFlags) > 0 {
shellSyncCmd += " " + strings.Join(syncFlags, " ")
}
shellCmd := shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
var shellCmd string
if autologinOnly {
shellCmd = shellSyncCmd + `; echo; echo "Auto-login update finished. Closing in 3 seconds..."; sleep 3`
} else {
shellCmd = shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
}
return runCommandInTerminal(shellCmd)
}
@@ -552,6 +569,49 @@ func resolveLocalWrapperShell() (string, error) {
return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper")
}
func syncGreeterAutoLoginOnly(nonInteractive bool) error {
logFunc := func(msg string) {
fmt.Println(msg)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
cacheSettingsPath := filepath.Join(greeter.GreeterCacheDir, "settings.json")
enabled := false
for _, path := range []string{cacheSettingsPath, settingsPath} {
data, readErr := os.ReadFile(path)
if readErr != nil {
continue
}
var cfg struct {
GreeterAutoLogin bool `json:"greeterAutoLogin"`
}
if json.Unmarshal(data, &cfg) == nil {
enabled = cfg.GreeterAutoLogin
break
}
}
fmt.Println("=== Greeter Auto-Login ===")
fmt.Println()
if enabled {
fmt.Println("Enabling auto-login on startup in greetd.")
fmt.Println("After your next reboot, DMS will skip the greeter password until you sign out.")
} else {
fmt.Println("Disabling auto-login on startup in greetd.")
fmt.Println("After your next reboot, you will enter your password at the greeter again.")
}
fmt.Println()
fmt.Println("Administrator (sudo) access is required to update /etc/greetd/config.toml.")
fmt.Println()
return greeter.SyncGreeterAutoLoginOnly(logFunc, "")
}
func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error {
if profileOnly {
return syncGreeterProfileOnly(nonInteractive)
+431 -19
View File
@@ -192,6 +192,421 @@ func upsertDefaultSession(configContent, greeterUser, command string) string {
return strings.Join(out, "\n")
}
func removeTomlSection(configContent, sectionName string) string {
lines := strings.Split(configContent, "\n")
var out []string
inSection := false
for _, line := range lines {
if section, ok := parseTomlSection(line); ok {
inSection = section == sectionName
if inSection {
continue
}
out = append(out, line)
continue
}
if inSection {
continue
}
out = append(out, line)
}
result := strings.TrimRight(strings.Join(out, "\n"), "\n")
if result != "" {
result += "\n"
}
return result
}
func stripDesktopExecCodes(execLine string) string {
fields := strings.Fields(execLine)
cleaned := make([]string, 0, len(fields))
for _, field := range fields {
if strings.HasPrefix(field, "%") {
continue
}
cleaned = append(cleaned, field)
}
return strings.Join(cleaned, " ")
}
func formatInitialSessionCommand(sessionExec string) string {
execLine := strings.TrimSpace(stripDesktopExecCodes(sessionExec))
if execLine == "" {
return `command = ""`
}
escaped := strings.ReplaceAll(execLine, `'`, `'\''`)
inner := fmt.Sprintf("env XDG_SESSION_TYPE=wayland sh -c 'exec %s'", escaped)
tomlEscaped := strings.ReplaceAll(inner, `\`, `\\`)
tomlEscaped = strings.ReplaceAll(tomlEscaped, `"`, `\"`)
return fmt.Sprintf(`command = "%s"`, tomlEscaped)
}
func upsertInitialSession(configContent, loginUser, sessionExec string, enabled bool) string {
if !enabled {
return removeTomlSection(configContent, "initial_session")
}
commandLine := formatInitialSessionCommand(sessionExec)
lines := strings.Split(configContent, "\n")
var out []string
inInitialSession := false
foundInitialSession := false
initialSessionUserSet := false
initialSessionCommandSet := false
appendInitialSessionFields := func() {
if !initialSessionUserSet {
out = append(out, fmt.Sprintf(`user = "%s"`, loginUser))
}
if !initialSessionCommandSet {
out = append(out, commandLine)
}
}
for _, line := range lines {
if section, ok := parseTomlSection(line); ok {
if inInitialSession {
appendInitialSessionFields()
}
inInitialSession = section == "initial_session"
if inInitialSession {
foundInitialSession = true
initialSessionUserSet = false
initialSessionCommandSet = false
}
out = append(out, line)
continue
}
if inInitialSession {
trimmed := stripTomlComment(line)
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
out = append(out, fmt.Sprintf(`user = "%s"`, loginUser))
initialSessionUserSet = true
continue
}
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
if !initialSessionCommandSet {
out = append(out, commandLine)
initialSessionCommandSet = true
}
continue
}
}
out = append(out, line)
}
if inInitialSession {
appendInitialSessionFields()
}
if !foundInitialSession {
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
out = append(out, "")
}
out = append(out, "[initial_session]")
out = append(out, fmt.Sprintf(`user = "%s"`, loginUser))
out = append(out, commandLine)
}
return strings.Join(out, "\n")
}
type greeterAutoLoginConfig struct {
GreeterAutoLogin bool `json:"greeterAutoLogin"`
GreeterRememberLastUser bool `json:"greeterRememberLastUser"`
GreeterRememberLastSession bool `json:"greeterRememberLastSession"`
}
type greeterAutoLoginMemory struct {
LastSuccessfulUser string `json:"lastSuccessfulUser"`
LastSessionID string `json:"lastSessionId"`
LastSessionExec string `json:"lastSessionExec"`
AutoLoginEnabled bool `json:"autoLoginEnabled"`
}
func readGreeterAutoLoginConfig(settingsPath string) (greeterAutoLoginConfig, error) {
cfg := greeterAutoLoginConfig{
GreeterRememberLastUser: true,
GreeterRememberLastSession: true,
}
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
}
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
}
return cfg, nil
}
func readGreeterAutoLoginMemory(memoryPath string) (greeterAutoLoginMemory, error) {
var mem greeterAutoLoginMemory
data, err := os.ReadFile(memoryPath)
if err != nil {
if os.IsNotExist(err) {
return mem, nil
}
return mem, err
}
if err := json.Unmarshal(data, &mem); err != nil {
return mem, fmt.Errorf("failed to parse greeter memory at %s: %w", memoryPath, err)
}
return mem, nil
}
func execFromDesktopFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "Exec=") {
return strings.TrimSpace(trimmed[len("Exec="):]), nil
}
}
return "", fmt.Errorf("no Exec= line found in %s", path)
}
func resolveGreeterAutoLoginState(cacheDir, homeDir string) (enabled bool, loginUser string, sessionExec string, err error) {
settingsPath := filepath.Join(cacheDir, "settings.json")
if _, statErr := os.Stat(settingsPath); statErr != nil {
settingsPath = filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
}
cfg, err := readGreeterAutoLoginConfig(settingsPath)
if err != nil {
return false, "", "", err
}
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
mem, err := readGreeterAutoLoginMemory(memoryPath)
if err != nil {
return false, "", "", err
}
enabled = cfg.GreeterAutoLogin
if !enabled {
return false, "", "", nil
}
if !cfg.GreeterRememberLastUser || !cfg.GreeterRememberLastSession {
return true, "", "", nil
}
loginUser = mem.LastSuccessfulUser
if loginUser == "" {
current, userErr := user.Current()
if userErr != nil {
return true, "", "", userErr
}
loginUser = current.Username
}
sessionExec = mem.LastSessionExec
if sessionExec == "" && mem.LastSessionID != "" {
sessionExec, err = execFromDesktopFile(mem.LastSessionID)
if err != nil {
sessionExec = ""
}
}
return true, loginUser, sessionExec, nil
}
func writeGreetdConfig(configPath, content string, logFunc func(string), sudoPassword, successMsg string) error {
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup greetd config: %w", err)
}
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(content); 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 := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
return fmt.Errorf("failed to create /etc/greetd: %w", err)
}
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to install greetd config: %w", err)
}
if logFunc != nil && successMsg != "" {
logFunc(successMsg)
}
return nil
}
func clearGreeterAutoLoginMemory(memoryPath, sudoPassword string) error {
data, err := readGreeterMemoryFile(memoryPath, sudoPassword)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if len(strings.TrimSpace(string(data))) == 0 {
return nil
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("failed to parse greeter memory at %s: %w", memoryPath, err)
}
if _, ok := raw["autoLoginEnabled"]; !ok {
return nil
}
delete(raw, "autoLoginEnabled")
encoded, err := json.MarshalIndent(raw, "", " ")
if err != nil {
return err
}
if len(encoded) == 0 || string(encoded) == "null" {
encoded = []byte("{}")
}
encoded = append(encoded, '\n')
if err := os.WriteFile(memoryPath, encoded, 0o644); err == nil {
return nil
} else if !os.IsPermission(err) {
return err
}
tmpFile, err := os.CreateTemp("", "greeter-memory-*.json")
if err != nil {
return fmt.Errorf("failed to create temp greeter memory file: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.Write(encoded); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write temp greeter memory file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp greeter memory file: %w", err)
}
greeterUser := DetectGreeterUser()
greeterGroup := DetectGreeterGroup()
owner := greeterUser + ":" + greeterGroup
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", greeterUser, "-g", greeterGroup, "-m", "0664", tmpFile.Name(), memoryPath); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0664", tmpFile.Name(), memoryPath); fallbackErr != nil {
return fmt.Errorf("failed to install greeter memory file (preferred %s: %w; fallback root:%s: %v)", owner, err, greeterGroup, fallbackErr)
}
}
return nil
}
func readGreeterMemoryFile(memoryPath, sudoPassword string) ([]byte, error) {
data, err := os.ReadFile(memoryPath)
if err == nil || !os.IsPermission(err) {
return data, err
}
tmpFile, err := os.CreateTemp("", "greeter-memory-read-*")
if err != nil {
return nil, fmt.Errorf("failed to create temp file for greeter memory read: %w", err)
}
tmpPath := tmpFile.Name()
_ = tmpFile.Close()
defer os.Remove(tmpPath)
if err := privesc.Run(context.Background(), sudoPassword, "cp", "-f", memoryPath, tmpPath); err != nil {
return nil, fmt.Errorf("failed to read greeter memory at %s: %w", memoryPath, err)
}
return os.ReadFile(tmpPath)
}
func SyncGreetdAutoLogin(cacheDir, homeDir string, logFunc func(string), sudoPassword string) error {
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
return err
}
configPath := "/etc/greetd/config.toml"
configContent := ""
if data, readErr := os.ReadFile(configPath); readErr == nil {
configContent = string(data)
} else if !os.IsNotExist(readErr) {
return fmt.Errorf("failed to read greetd config: %w", readErr)
}
if !enabled {
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
if err := clearGreeterAutoLoginMemory(memoryPath, sudoPassword); err != nil && logFunc != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to clear greeter auto-login memory flag: %v", err))
}
newConfig := upsertInitialSession(configContent, "", "", false)
if newConfig == configContent {
if logFunc != nil {
logFunc("✓ Greeter auto-login disabled")
}
return nil
}
return writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, "✓ Disabled greeter auto-login")
}
if loginUser == "" || sessionExec == "" {
if logFunc != nil {
logFunc("⚠ Greeter auto-login is enabled but user or session is not configured yet. Log in manually once, then run sync.")
}
newConfig := upsertInitialSession(configContent, "", "", false)
if newConfig != configContent {
return writeGreetdConfig(configPath, newConfig, nil, sudoPassword, "")
}
return nil
}
newConfig := upsertInitialSession(configContent, loginUser, sessionExec, true)
if newConfig == configContent {
if logFunc != nil {
logFunc(fmt.Sprintf("✓ Greeter auto-login already configured for %s", loginUser))
}
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
_ = clearGreeterAutoLoginMemory(memoryPath, sudoPassword)
return nil
}
if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("✓ Configured greeter auto-login for %s", loginUser)); err != nil {
return err
}
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
if err := clearGreeterAutoLoginMemory(memoryPath, sudoPassword); err != nil && logFunc != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to clear greeter auto-login memory flag: %v", err))
}
return nil
}
func SyncGreeterAutoLoginOnly(logFunc func(string), sudoPassword string) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
return SyncGreetdAutoLogin(GreeterCacheDir, homeDir, logFunc, sudoPassword)
}
func DetectGreeterUser() string {
passwdData, err := os.ReadFile("/etc/passwd")
if err == nil {
@@ -1267,6 +1682,10 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
return fmt.Errorf("per-user greeter cache sync failed: %w", err)
}
if err := SyncGreetdAutoLogin(cacheDir, homeDir, logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: greeter auto-login sync failed: %v", err))
}
if strings.ToLower(compositor) != "niri" {
return nil
}
@@ -1731,29 +2150,22 @@ vt = 1
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)
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
enabled, loginUser, sessionExec, resolveErr := resolveGreeterAutoLoginState(GreeterCacheDir, homeDir)
if resolveErr != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to resolve greeter auto-login state: %v", resolveErr))
} else if enabled && loginUser != "" && sessionExec != "" {
newConfig = upsertInitialSession(newConfig, loginUser, sessionExec, true)
} else {
newConfig = upsertInitialSession(newConfig, "", "", false)
}
}
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
return fmt.Errorf("failed to create /etc/greetd: %w", err)
if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue)); err != nil {
return err
}
if err := privesc.Run(context.Background(), 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
}
+145
View File
@@ -3,6 +3,7 @@ package greeter
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -96,3 +97,147 @@ func TestResolveGreeterThemeSyncState(t *testing.T) {
})
}
}
func TestUpsertInitialSession(t *testing.T) {
t.Parallel()
baseConfig := `[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command niri"
`
t.Run("inserts initial session", func(t *testing.T) {
t.Parallel()
got := upsertInitialSession(baseConfig, "alice", "niri", true)
if !strings.Contains(got, "[initial_session]") {
t.Fatalf("expected [initial_session] section, got:\n%s", got)
}
if !strings.Contains(got, `user = "alice"`) {
t.Fatalf("expected alice user in initial session, got:\n%s", got)
}
if !strings.Contains(got, `env XDG_SESSION_TYPE=wayland sh -c 'exec niri'`) {
t.Fatalf("expected wrapped session command, got:\n%s", got)
}
})
t.Run("updates existing initial session", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "bob"
command = "old-command"
`
got := upsertInitialSession(existing, "alice", "Hyprland", true)
if strings.Contains(got, `user = "bob"`) {
t.Fatalf("expected bob to be replaced, got:\n%s", got)
}
if !strings.Contains(got, `exec Hyprland`) {
t.Fatalf("expected Hyprland command, got:\n%s", got)
}
})
t.Run("removes initial session when disabled", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "alice"
command = "niri"
`
got := upsertInitialSession(existing, "", "", false)
if strings.Contains(got, "[initial_session]") {
t.Fatalf("expected initial session removed, got:\n%s", got)
}
if !strings.Contains(got, "[default_session]") {
t.Fatalf("expected default session preserved, got:\n%s", got)
}
})
}
func TestStripDesktopExecCodes(t *testing.T) {
t.Parallel()
got := stripDesktopExecCodes("niri --session %f")
want := "niri --session"
if got != want {
t.Fatalf("stripDesktopExecCodes = %q, want %q", got, want)
}
}
func TestResolveGreeterAutoLoginState(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": true,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"lastSuccessfulUser": "alice",
"lastSessionExec": "niri"
}`)
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if !enabled || loginUser != "alice" || sessionExec != "niri" {
t.Fatalf("got enabled=%v user=%q exec=%q", enabled, loginUser, sessionExec)
}
}
func TestResolveGreeterAutoLoginStateIgnoresMemoryFlag(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": false,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice",
"lastSessionExec": "niri"
}`)
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if enabled || loginUser != "" || sessionExec != "" {
t.Fatalf("expected disabled with empty user/exec, got enabled=%v user=%q exec=%q", enabled, loginUser, sessionExec)
}
}
func TestClearGreeterAutoLoginMemory(t *testing.T) {
t.Parallel()
memoryPath := filepath.Join(t.TempDir(), "memory.json")
writeTestFile(t, memoryPath, `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice"
}`)
if err := clearGreeterAutoLoginMemory(memoryPath, ""); err != nil {
t.Fatalf("clearGreeterAutoLoginMemory returned error: %v", err)
}
data, err := os.ReadFile(memoryPath)
if err != nil {
t.Fatalf("failed to read memory file: %v", err)
}
if strings.Contains(string(data), "autoLoginEnabled") {
t.Fatalf("expected autoLoginEnabled removed, got: %s", string(data))
}
if !strings.Contains(string(data), "lastSuccessfulUser") {
t.Fatalf("expected other memory fields preserved, got: %s", string(data))
}
}
+12 -1
View File
@@ -414,6 +414,7 @@ Singleton {
property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property bool greeterAutoLogin: false
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
@@ -1333,6 +1334,15 @@ Singleton {
});
}
function scheduleGreeterAutoLoginSync() {
if (isGreeterMode)
return;
Qt.callLater(() => {
Processes.settingsRoot = root;
Processes.scheduleGreeterAutoLoginSync();
});
}
readonly property var _hooks: ({
"applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes,
@@ -1340,7 +1350,8 @@ Singleton {
"applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor,
"scheduleAuthApply": scheduleAuthApply
"scheduleAuthApply": scheduleAuthApply,
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync
})
function set(key, value) {
+165
View File
@@ -12,6 +12,35 @@ Singleton {
property var settingsRoot: null
onSettingsRootChanged: {
if (settingsRoot && !settingsRoot.isGreeterMode)
consumeGreeterAutoLoginPendingSync();
}
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
function consumeGreeterAutoLoginPendingSync() {
if (!settingsRoot || settingsRoot.isGreeterMode)
return;
greeterAutoLoginPendingCheckProcess.running = true;
}
property var greeterAutoLoginPendingCheckProcess: Process {
command: ["sh", "-c", "if [ -f " + JSON.stringify(root.greeterAutoLoginPendingSyncPath) + " ]; then rm -f " + JSON.stringify(root.greeterAutoLoginPendingSyncPath) + "; echo pending; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if ((text || "").trim() !== "pending" || !root.settingsRoot)
return;
if (!root.settingsRoot.greeterAutoLogin)
root.settingsRoot.set("greeterAutoLogin", true);
else
root.scheduleGreeterAutoLoginSync();
}
}
}
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
@@ -296,6 +325,66 @@ Singleton {
authApplyDebounce.restart();
}
// --- Greeter auto-login sync pipeline ---
property bool greeterAutoLoginSyncRunning: false
property bool greeterAutoLoginSyncQueued: false
property bool greeterAutoLoginSyncRerunRequested: false
property string greeterAutoLoginSyncStdout: ""
property string greeterAutoLoginSyncStderr: ""
property string greeterAutoLoginSyncTerminalFallbackStderr: ""
function scheduleGreeterAutoLoginSync() {
if (!settingsRoot || settingsRoot.isGreeterMode)
return;
greeterAutoLoginSyncQueued = true;
if (greeterAutoLoginSyncRunning) {
greeterAutoLoginSyncRerunRequested = true;
return;
}
greeterAutoLoginSyncDebounce.restart();
}
function beginGreeterAutoLoginSync() {
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot || settingsRoot.isGreeterMode)
return;
greeterAutoLoginSyncQueued = false;
greeterAutoLoginSyncRerunRequested = false;
greeterAutoLoginSyncStdout = "";
greeterAutoLoginSyncStderr = "";
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncRunning = true;
greeterAutoLoginSyncSudoProbeProcess.running = true;
}
function launchGreeterAutoLoginSyncTerminalFallback(details) {
ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin-only", "greeter-autologin-sync");
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncTerminalFallbackProcess.running = true;
}
function greeterAutoLoginSyncSuccessToast(details) {
const enabling = settingsRoot && settingsRoot.greeterAutoLogin;
// Clear the sticky in-progress toast, then confirm with an auto-dismissing toast.
ToastService.dismissCategory("greeter-autologin-sync");
if (enabling) {
ToastService.showWarning(I18n.tr("Auto-login enabled"), I18n.tr("You'll skip the greeter password after the next reboot. The lock screen and signing out still require your password.") + (details ? "\n\n" + details : ""));
} else {
ToastService.showInfo(I18n.tr("Auto-login disabled"), I18n.tr("You'll enter your password at the greeter after the next reboot.") + (details ? "\n\n" + details : ""));
}
}
function finishGreeterAutoLoginSync() {
const shouldRerun = greeterAutoLoginSyncQueued || greeterAutoLoginSyncRerunRequested;
greeterAutoLoginSyncRunning = false;
greeterAutoLoginSyncRerunRequested = false;
if (shouldRerun)
greeterAutoLoginSyncDebounce.restart();
}
// --- PAM parsing helpers ---
function stripPamComment(line) {
@@ -433,6 +522,82 @@ Singleton {
onTriggered: root.beginAuthApply()
}
Timer {
id: greeterAutoLoginSyncDebounce
interval: 300
repeat: false
onTriggered: root.beginGreeterAutoLoginSync()
}
property var greeterAutoLoginSyncProcess: Process {
command: ["dms", "greeter", "sync", "--yes", "--autologin-only"]
running: false
stdout: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncStdout = text || ""
}
stderr: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncStderr = text || ""
}
onExited: exitCode => {
const out = (root.greeterAutoLoginSyncStdout || "").trim();
const err = (root.greeterAutoLoginSyncStderr || "").trim();
if (exitCode === 0) {
let details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
root.greeterAutoLoginSyncSuccessToast(details);
root.finishGreeterAutoLoginSync();
return;
}
let details = "";
if (out !== "")
details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
root.launchGreeterAutoLoginSyncTerminalFallback(details);
}
}
property var greeterAutoLoginSyncSudoProbeProcess: Process {
command: ["sudo", "-n", "true"]
running: false
onExited: exitCode => {
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
if (exitCode === 0) {
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup…") : I18n.tr("Disabling auto-login on startup…"), "", "dms greeter sync --autologin-only", "greeter-autologin-sync");
root.greeterAutoLoginSyncProcess.running = true;
return;
}
root.launchGreeterAutoLoginSyncTerminalFallback();
}
}
property var greeterAutoLoginSyncTerminalFallbackProcess: Process {
command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin-only"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncTerminalFallbackStderr = text || ""
}
onExited: exitCode => {
if (exitCode === 0) {
root.greeterAutoLoginSyncSuccessToast("");
} else {
let details = (root.greeterAutoLoginSyncTerminalFallbackStderr || "").trim();
ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin-only", "greeter-autologin-sync");
}
root.finishGreeterAutoLoginSync();
}
}
property var authApplyProcess: Process {
command: ["dms", "auth", "sync", "--yes"]
running: false
@@ -182,6 +182,7 @@ var SPEC = {
lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" },
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
greeterWallpaperPath: { def: "" },
+20 -1
View File
@@ -18,6 +18,7 @@ Singleton {
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
property string lastSessionId: ""
property string lastSessionExec: ""
property string lastSuccessfulUser: ""
property bool memoryReady: false
property bool isLightMode: false
@@ -54,6 +55,7 @@ Singleton {
return;
const memory = JSON.parse(content);
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
lastSessionExec = rememberLastSession ? (memory.lastSessionExec || "") : "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
@@ -66,6 +68,8 @@ Singleton {
let memory = {};
if (rememberLastSession && lastSessionId)
memory.lastSessionId = lastSessionId;
if (rememberLastSession && lastSessionExec)
memory.lastSessionExec = lastSessionExec;
if (rememberLastUser && lastSuccessfulUser)
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
@@ -73,13 +77,28 @@ Singleton {
function setLastSessionId(id) {
if (!rememberLastSession) {
if (lastSessionId !== "") {
if (lastSessionId !== "" || lastSessionExec !== "") {
lastSessionId = "";
lastSessionExec = "";
saveMemory();
}
return;
}
lastSessionId = id || "";
if (!lastSessionId)
lastSessionExec = "";
saveMemory();
}
function setLastSessionExec(exec) {
if (!rememberLastSession) {
if (lastSessionExec !== "") {
lastSessionExec = "";
saveMemory();
}
return;
}
lastSessionExec = exec || "";
saveMemory();
}
@@ -67,6 +67,7 @@ Singleton {
property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool greeterAutoLogin: false
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
@@ -132,6 +133,9 @@ Singleton {
} else {
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
}
if (configBaseDir === root._greeterCacheDir) {
greeterAutoLogin = settings.greeterAutoLogin !== undefined ? settings.greeterAutoLogin : false;
}
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
+240 -127
View File
@@ -57,6 +57,7 @@ Item {
property int maxPasswordSessionTransitionRetries: 2
property bool fprintdProbeComplete: false
property bool fprintdHasDevice: false
property bool autoLoginOnSuccess: 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")
@@ -524,6 +525,7 @@ Item {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
root.autoLoginOnSuccess = false;
}
root.pickerThemeUsername = user;
GreeterState.username = user;
@@ -646,6 +648,12 @@ Item {
}
}
Process {
id: greeterAutoLoginPendingProcess
command: ["sh", "-c", "mkdir -p $(dirname " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending") + ") && touch " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending")]
running: false
}
Process {
id: hyprlandLayoutProcess
running: false
@@ -870,110 +878,110 @@ Item {
anchors.top: parent.top
spacing: 0
property string fullTimeStr: {
const format = GreetdSettings.getEffectiveTimeFormat();
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
}
property var timeParts: fullTimeStr.split(':')
property string hours: timeParts[0] || ""
property string minutes: timeParts[1] || ""
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
property string ampm: {
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
return match ? match[0].trim() : "";
}
property bool hasSeconds: timeParts.length > 2
property string fullTimeStr: {
const format = GreetdSettings.getEffectiveTimeFormat();
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
}
property var timeParts: fullTimeStr.split(':')
property string hours: timeParts[0] || ""
property string minutes: timeParts[1] || ""
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
property string ampm: {
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
return match ? match[0].trim() : "";
}
property bool hasSeconds: timeParts.length > 2
StyledText {
width: 75
text: clockText.hours.length > 1 ? clockText.hours[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.hours.length > 1 ? clockText.hours[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: ":"
font.pixelSize: 120
font.weight: Font.Light
color: "white"
}
StyledText {
text: ":"
font.pixelSize: 120
font.weight: Font.Light
color: "white"
}
StyledText {
width: 75
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: 75
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: clockText.hasSeconds ? ":" : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
visible: clockText.hasSeconds
}
StyledText {
text: clockText.hasSeconds ? ":" : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
visible: clockText.hasSeconds
}
StyledText {
width: 75
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
visible: clockText.hasSeconds
}
StyledText {
width: 75
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
visible: clockText.hasSeconds
}
StyledText {
width: 75
text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
visible: clockText.hasSeconds
}
StyledText {
width: 75
text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
font.pixelSize: 120
font.weight: Font.Light
color: "white"
horizontalAlignment: Text.AlignHCenter
visible: clockText.hasSeconds
}
StyledText {
width: 20
text: " "
font.pixelSize: 120
font.weight: Font.Light
color: "white"
visible: clockText.ampm !== ""
}
StyledText {
width: 20
text: " "
font.pixelSize: 120
font.weight: Font.Light
color: "white"
visible: clockText.ampm !== ""
}
StyledText {
text: clockText.ampm
font.pixelSize: 120
font.weight: Font.Light
color: "white"
visible: clockText.ampm !== ""
StyledText {
text: clockText.ampm
font.pixelSize: 120
font.weight: Font.Light
color: "white"
visible: clockText.ampm !== ""
}
}
}
}
StyledText {
id: dateText
@@ -1355,7 +1363,7 @@ Item {
StyledText {
Layout.fillWidth: true
Layout.preferredHeight: 38
Layout.preferredHeight: root.authFeedbackMessage !== "" ? 38 : 0
Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS
text: root.authFeedbackMessage
@@ -1374,48 +1382,150 @@ Item {
}
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 0
Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2
Layout.preferredHeight: 40
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: GreeterState.showPasswordInput ? 1 : 0
enabled: GreeterState.showPasswordInput
// Password-screen actions: Switch User + Auto-login toggle as one compact chip row
Item {
id: passwordActions
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
readonly property bool autoLoginAvailable: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession
Layout.fillWidth: true
Layout.topMargin: Theme.spacingXS
Layout.preferredHeight: visible ? 32 : 0
visible: GreeterState.showPasswordInput && !GreeterState.unlocking && (root.multipleUsersAvailable || autoLoginAvailable)
Row {
id: switchUserRow
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "people"
size: Theme.iconSize - 4
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: switchUserChip
visible: root.multipleUsersAvailable
height: 32
width: switchUserContent.implicitWidth + Theme.spacingM * 2
radius: height / 2
color: Theme.withAlpha(Theme.surfaceVariant, 0.65)
Rectangle {
anchors.fill: parent
radius: parent.radius
color: (switchUserMouse.containsMouse || switchUserMouse.pressed) ? Theme.surfaceTextHover : "transparent"
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: switchUserRipple
cornerRadius: switchUserChip.radius
rippleColor: Theme.surfaceVariantText
}
Row {
id: switchUserContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "people"
size: 16
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Switch User")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: switchUserMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => switchUserRipple.trigger(mouse.x, mouse.y)
onClicked: root.returnToUserPicker()
}
}
StyledText {
text: I18n.tr("Switch User")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
id: autoLoginChip
StateLayer {
stateColor: Theme.primary
cornerRadius: parent.radius
enabled: !GreeterState.unlocking && GreeterState.showPasswordInput
onClicked: root.returnToUserPicker()
visible: passwordActions.autoLoginAvailable
height: 32
width: autoLoginContent.implicitWidth + Theme.spacingM * 2
radius: height / 2
color: root.autoLoginOnSuccess ? Theme.withAlpha(Theme.primary, 0.85) : Theme.withAlpha(Theme.surfaceVariant, 0.65)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: {
if (autoLoginMouse.pressed)
return root.autoLoginOnSuccess ? Theme.primaryPressed : Theme.surfaceTextHover;
if (autoLoginMouse.containsMouse)
return root.autoLoginOnSuccess ? Theme.primaryHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: autoLoginRipple
cornerRadius: autoLoginChip.radius
rippleColor: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText
}
Row {
id: autoLoginContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: root.autoLoginOnSuccess ? "check" : "login"
size: 16
color: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Auto-login")
font.pixelSize: Theme.fontSizeSmall
font.weight: root.autoLoginOnSuccess ? Font.Medium : Font.Normal
color: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: autoLoginMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.autoLoginOnSuccess = !root.autoLoginOnSuccess
onPressed: mouse => autoLoginRipple.trigger(mouse.x, mouse.y)
}
}
}
}
}
@@ -1984,7 +2094,8 @@ Item {
launchTimeout.restart();
if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSessionId(sessionPath);
} else if (GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionExec(sessionCmd);
} else if (GreetdMemory.lastSessionId || GreetdMemory.lastSessionExec) {
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
@@ -1992,6 +2103,8 @@ Item {
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
if (root.autoLoginOnSuccess)
greeterAutoLoginPendingProcess.running = true;
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
memoryFlushTimer.restart();
@@ -743,6 +743,16 @@ Item {
checked: SettingsData.greeterRememberLastUser
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
}
SettingsToggleRow {
settingKey: "greeterAutoLogin"
tags: ["greeter", "autologin", "login", "startup", "password"]
text: I18n.tr("Auto-login on startup")
description: SettingsData.greeterRememberLastUser && SettingsData.greeterRememberLastSession ? I18n.tr("Skip the greeter password after boot until you sign out. Lock screen unlock is unchanged. Takes effect on the next reboot after sync.") : I18n.tr("Requires remembering the last user and session. Enable those options first.")
checked: SettingsData.greeterAutoLogin
enabled: SettingsData.greeterRememberLastUser && SettingsData.greeterRememberLastSession
onToggled: checked => SettingsData.set("greeterAutoLogin", checked)
}
}
SettingsCard {
+3 -3
View File
@@ -50,7 +50,7 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
readonly property real toastWidth: shouldBeVisible ? Theme.px(Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + (ToastService.hasDetails ? (expandButton.width + closeButton.width + 4) : (ToastService.currentLevel === ToastService.levelError ? closeButton.width + Theme.spacingS : 0)) + Theme.spacingL * 2 + Theme.spacingM * 2), dpr) : frozenWidth
readonly property real toastWidth: shouldBeVisible ? Theme.px(Math.min(900, messageText.implicitWidth + statusIcon.width + Theme.spacingM + ((ToastService.hasDetails || ToastService.isStickyCategory(ToastService.currentCategory)) ? (expandButton.width + closeButton.width + 4) : (ToastService.currentLevel === ToastService.levelError ? closeButton.width + Theme.spacingS : 0)) + Theme.spacingL * 2 + Theme.spacingM * 2), dpr) : frozenWidth
readonly property real toastHeight: Theme.px(toastContent.height + Theme.spacingL * 2, dpr)
anchors {
@@ -208,7 +208,7 @@ PanelWindow {
buttonSize: Theme.iconSize + 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: ToastService.hasDetails || ToastService.currentLevel === ToastService.levelError
visible: ToastService.hasDetails || ToastService.currentLevel === ToastService.levelError || ToastService.isStickyCategory(ToastService.currentCategory)
onClicked: {
ToastService.hideToast();
@@ -400,7 +400,7 @@ PanelWindow {
MouseArea {
anchors.fill: parent
visible: !ToastService.hasDetails
visible: !ToastService.hasDetails && !ToastService.isStickyCategory(ToastService.currentCategory)
onClicked: ToastService.hideToast()
}
+11 -1
View File
@@ -23,6 +23,11 @@ Singleton {
property var lastErrorTime: ({})
property int errorThrottleMs: 1000
property string currentCategory: ""
readonly property var stickyCategories: ["greeter-autologin-sync"]
function isStickyCategory(category) {
return category && stickyCategories.indexOf(category) >= 0
}
function showToast(message, level = levelInfo, details = "", command = "", category = "") {
const now = Date.now()
@@ -137,7 +142,9 @@ Singleton {
toastVisible = true
resetToastState()
if (toast.level === levelError && hasDetails) {
if (isStickyCategory(toast.category)) {
toastTimer.stop()
} else if (toast.level === levelError && hasDetails) {
toastTimer.interval = 8000
toastTimer.start()
} else {
@@ -153,6 +160,9 @@ Singleton {
}
function restartTimer() {
if (isStickyCategory(currentCategory)) {
return
}
if (hasDetails && currentLevel === levelError) {
toastTimer.interval = 8000
toastTimer.restart()