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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
}
|
||||
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 := 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: "" },
|
||||
|
||||
@@ -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 : "";
|
||||
|
||||
@@ -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
|
||||
@@ -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,50 +1382,152 @@ 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
|
||||
|
||||
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: Theme.iconSize - 4
|
||||
color: Theme.surfaceText
|
||||
size: 16
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Switch User")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
enabled: !GreeterState.unlocking && GreeterState.showPasswordInput
|
||||
MouseArea {
|
||||
id: switchUserMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => switchUserRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: root.returnToUserPicker()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: autoLoginChip
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user