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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -74,13 +75,20 @@ var greeterSyncCmd = &cobra.Command{
|
|||||||
auth, _ := cmd.Flags().GetBool("auth")
|
auth, _ := cmd.Flags().GetBool("auth")
|
||||||
local, _ := cmd.Flags().GetBool("local")
|
local, _ := cmd.Flags().GetBool("local")
|
||||||
profile, _ := cmd.Flags().GetBool("profile")
|
profile, _ := cmd.Flags().GetBool("profile")
|
||||||
|
autologinOnly, _ := cmd.Flags().GetBool("autologin-only")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
if term {
|
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)
|
log.Fatalf("Error launching sync in terminal: %v", err)
|
||||||
}
|
}
|
||||||
return
|
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 {
|
if err := syncGreeter(yes, auth, local, profile); err != nil {
|
||||||
log.Fatalf("Error syncing greeter: %v", err)
|
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("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("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().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{
|
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)")
|
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 {
|
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly bool, autologinOnly bool) error {
|
||||||
syncFlags := make([]string, 0, 4)
|
syncFlags := make([]string, 0, 5)
|
||||||
if nonInteractive {
|
if nonInteractive {
|
||||||
syncFlags = append(syncFlags, "--yes")
|
syncFlags = append(syncFlags, "--yes")
|
||||||
}
|
}
|
||||||
@@ -534,11 +543,19 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly
|
|||||||
if profileOnly {
|
if profileOnly {
|
||||||
syncFlags = append(syncFlags, "--profile")
|
syncFlags = append(syncFlags, "--profile")
|
||||||
}
|
}
|
||||||
|
if autologinOnly {
|
||||||
|
syncFlags = append(syncFlags, "--autologin-only")
|
||||||
|
}
|
||||||
shellSyncCmd := "dms greeter sync"
|
shellSyncCmd := "dms greeter sync"
|
||||||
if len(syncFlags) > 0 {
|
if len(syncFlags) > 0 {
|
||||||
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
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)
|
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")
|
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 {
|
func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error {
|
||||||
if profileOnly {
|
if profileOnly {
|
||||||
return syncGreeterProfileOnly(nonInteractive)
|
return syncGreeterProfileOnly(nonInteractive)
|
||||||
|
|||||||
@@ -192,6 +192,421 @@ func upsertDefaultSession(configContent, greeterUser, command string) string {
|
|||||||
return strings.Join(out, "\n")
|
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 {
|
func DetectGreeterUser() string {
|
||||||
passwdData, err := os.ReadFile("/etc/passwd")
|
passwdData, err := os.ReadFile("/etc/passwd")
|
||||||
if err == nil {
|
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)
|
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" {
|
if strings.ToLower(compositor) != "niri" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1731,29 +2150,22 @@ vt = 1
|
|||||||
commandLine := fmt.Sprintf(`command = "%s"`, commandValue)
|
commandLine := fmt.Sprintf(`command = "%s"`, commandValue)
|
||||||
newConfig := upsertDefaultSession(configContent, greeterUser, commandLine)
|
newConfig := upsertDefaultSession(configContent, greeterUser, commandLine)
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
|
homeDir, homeErr := os.UserHomeDir()
|
||||||
if err != nil {
|
if homeErr == nil {
|
||||||
return fmt.Errorf("failed to create temp greetd config: %w", err)
|
enabled, loginUser, sessionExec, resolveErr := resolveGreeterAutoLoginState(GreeterCacheDir, homeDir)
|
||||||
}
|
if resolveErr != nil {
|
||||||
defer os.Remove(tmpFile.Name())
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to resolve greeter auto-login state: %v", resolveErr))
|
||||||
|
} else if enabled && loginUser != "" && sessionExec != "" {
|
||||||
if _, err := tmpFile.WriteString(newConfig); err != nil {
|
newConfig = upsertInitialSession(newConfig, loginUser, sessionExec, true)
|
||||||
_ = tmpFile.Close()
|
} else {
|
||||||
return fmt.Errorf("failed to write temp greetd config: %w", err)
|
newConfig = upsertInitialSession(newConfig, "", "", false)
|
||||||
}
|
}
|
||||||
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 {
|
if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue)); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package greeter
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"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 string lockDateFormat: ""
|
||||||
property bool greeterRememberLastSession: true
|
property bool greeterRememberLastSession: true
|
||||||
property bool greeterRememberLastUser: true
|
property bool greeterRememberLastUser: true
|
||||||
|
property bool greeterAutoLogin: false
|
||||||
property bool greeterEnableFprint: false
|
property bool greeterEnableFprint: false
|
||||||
property bool greeterEnableU2f: false
|
property bool greeterEnableU2f: false
|
||||||
property string greeterWallpaperPath: ""
|
property string greeterWallpaperPath: ""
|
||||||
@@ -1333,6 +1334,15 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleGreeterAutoLoginSync() {
|
||||||
|
if (isGreeterMode)
|
||||||
|
return;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
Processes.settingsRoot = root;
|
||||||
|
Processes.scheduleGreeterAutoLoginSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"applyStoredTheme": applyStoredTheme,
|
"applyStoredTheme": applyStoredTheme,
|
||||||
"regenSystemThemes": regenSystemThemes,
|
"regenSystemThemes": regenSystemThemes,
|
||||||
@@ -1340,7 +1350,8 @@ Singleton {
|
|||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor,
|
||||||
"scheduleAuthApply": scheduleAuthApply
|
"scheduleAuthApply": scheduleAuthApply,
|
||||||
|
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
|
|||||||
@@ -12,6 +12,35 @@ Singleton {
|
|||||||
|
|
||||||
property var settingsRoot: null
|
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 greetdPamText: ""
|
||||||
property string systemAuthPamText: ""
|
property string systemAuthPamText: ""
|
||||||
property string commonAuthPamText: ""
|
property string commonAuthPamText: ""
|
||||||
@@ -296,6 +325,66 @@ Singleton {
|
|||||||
authApplyDebounce.restart();
|
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 ---
|
// --- PAM parsing helpers ---
|
||||||
|
|
||||||
function stripPamComment(line) {
|
function stripPamComment(line) {
|
||||||
@@ -433,6 +522,82 @@ Singleton {
|
|||||||
onTriggered: root.beginAuthApply()
|
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 {
|
property var authApplyProcess: Process {
|
||||||
command: ["dms", "auth", "sync", "--yes"]
|
command: ["dms", "auth", "sync", "--yes"]
|
||||||
running: false
|
running: false
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ var SPEC = {
|
|||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
greeterRememberLastSession: { def: true },
|
greeterRememberLastSession: { def: true },
|
||||||
greeterRememberLastUser: { def: true },
|
greeterRememberLastUser: { def: true },
|
||||||
|
greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" },
|
||||||
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||||
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
greeterWallpaperPath: { def: "" },
|
greeterWallpaperPath: { def: "" },
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Singleton {
|
|||||||
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
||||||
|
|
||||||
property string lastSessionId: ""
|
property string lastSessionId: ""
|
||||||
|
property string lastSessionExec: ""
|
||||||
property string lastSuccessfulUser: ""
|
property string lastSuccessfulUser: ""
|
||||||
property bool memoryReady: false
|
property bool memoryReady: false
|
||||||
property bool isLightMode: false
|
property bool isLightMode: false
|
||||||
@@ -54,6 +55,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
const memory = JSON.parse(content);
|
const memory = JSON.parse(content);
|
||||||
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
||||||
|
lastSessionExec = rememberLastSession ? (memory.lastSessionExec || "") : "";
|
||||||
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
||||||
if (!rememberLastSession || !rememberLastUser)
|
if (!rememberLastSession || !rememberLastUser)
|
||||||
saveMemory();
|
saveMemory();
|
||||||
@@ -66,6 +68,8 @@ Singleton {
|
|||||||
let memory = {};
|
let memory = {};
|
||||||
if (rememberLastSession && lastSessionId)
|
if (rememberLastSession && lastSessionId)
|
||||||
memory.lastSessionId = lastSessionId;
|
memory.lastSessionId = lastSessionId;
|
||||||
|
if (rememberLastSession && lastSessionExec)
|
||||||
|
memory.lastSessionExec = lastSessionExec;
|
||||||
if (rememberLastUser && lastSuccessfulUser)
|
if (rememberLastUser && lastSuccessfulUser)
|
||||||
memory.lastSuccessfulUser = lastSuccessfulUser;
|
memory.lastSuccessfulUser = lastSuccessfulUser;
|
||||||
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
||||||
@@ -73,13 +77,28 @@ Singleton {
|
|||||||
|
|
||||||
function setLastSessionId(id) {
|
function setLastSessionId(id) {
|
||||||
if (!rememberLastSession) {
|
if (!rememberLastSession) {
|
||||||
if (lastSessionId !== "") {
|
if (lastSessionId !== "" || lastSessionExec !== "") {
|
||||||
lastSessionId = "";
|
lastSessionId = "";
|
||||||
|
lastSessionExec = "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastSessionId = id || "";
|
lastSessionId = id || "";
|
||||||
|
if (!lastSessionId)
|
||||||
|
lastSessionExec = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLastSessionExec(exec) {
|
||||||
|
if (!rememberLastSession) {
|
||||||
|
if (lastSessionExec !== "") {
|
||||||
|
lastSessionExec = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSessionExec = exec || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ Singleton {
|
|||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
property bool rememberLastSession: true
|
property bool rememberLastSession: true
|
||||||
property bool rememberLastUser: true
|
property bool rememberLastUser: true
|
||||||
|
property bool greeterAutoLogin: false
|
||||||
property bool greeterEnableFprint: false
|
property bool greeterEnableFprint: false
|
||||||
property bool greeterEnableU2f: false
|
property bool greeterEnableU2f: false
|
||||||
property string greeterWallpaperPath: ""
|
property string greeterWallpaperPath: ""
|
||||||
@@ -132,6 +133,9 @@ Singleton {
|
|||||||
} else {
|
} else {
|
||||||
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
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;
|
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
|
||||||
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
||||||
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ Item {
|
|||||||
property int maxPasswordSessionTransitionRetries: 2
|
property int maxPasswordSessionTransitionRetries: 2
|
||||||
property bool fprintdProbeComplete: false
|
property bool fprintdProbeComplete: false
|
||||||
property bool fprintdHasDevice: false
|
property bool fprintdHasDevice: false
|
||||||
|
property bool autoLoginOnSuccess: false
|
||||||
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
|
// 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 greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
|
||||||
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||||
@@ -524,6 +525,7 @@ Item {
|
|||||||
passwordFailureCount = 0;
|
passwordFailureCount = 0;
|
||||||
clearAuthFeedback();
|
clearAuthFeedback();
|
||||||
externalAuthAutoStartedForUser = "";
|
externalAuthAutoStartedForUser = "";
|
||||||
|
root.autoLoginOnSuccess = false;
|
||||||
}
|
}
|
||||||
root.pickerThemeUsername = user;
|
root.pickerThemeUsername = user;
|
||||||
GreeterState.username = 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 {
|
Process {
|
||||||
id: hyprlandLayoutProcess
|
id: hyprlandLayoutProcess
|
||||||
running: false
|
running: false
|
||||||
@@ -870,110 +878,110 @@ Item {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
property string fullTimeStr: {
|
property string fullTimeStr: {
|
||||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||||
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
||||||
}
|
}
|
||||||
property var timeParts: fullTimeStr.split(':')
|
property var timeParts: fullTimeStr.split(':')
|
||||||
property string hours: timeParts[0] || ""
|
property string hours: timeParts[0] || ""
|
||||||
property string minutes: timeParts[1] || ""
|
property string minutes: timeParts[1] || ""
|
||||||
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
|
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
|
||||||
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
|
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
|
||||||
property string ampm: {
|
property string ampm: {
|
||||||
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
|
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
|
||||||
return match ? match[0].trim() : "";
|
return match ? match[0].trim() : "";
|
||||||
}
|
}
|
||||||
property bool hasSeconds: timeParts.length > 2
|
property bool hasSeconds: timeParts.length > 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 75
|
width: 75
|
||||||
text: clockText.hours.length > 1 ? clockText.hours[0] : ""
|
text: clockText.hours.length > 1 ? clockText.hours[0] : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 75
|
width: 75
|
||||||
text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
|
text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: ":"
|
text: ":"
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 75
|
width: 75
|
||||||
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
|
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 75
|
width: 75
|
||||||
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
|
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: clockText.hasSeconds ? ":" : ""
|
text: clockText.hasSeconds ? ":" : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
visible: clockText.hasSeconds
|
visible: clockText.hasSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 75
|
width: 75
|
||||||
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
|
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
visible: clockText.hasSeconds
|
visible: clockText.hasSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 75
|
width: 75
|
||||||
text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
|
text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
visible: clockText.hasSeconds
|
visible: clockText.hasSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: 20
|
width: 20
|
||||||
text: " "
|
text: " "
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
visible: clockText.ampm !== ""
|
visible: clockText.ampm !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: clockText.ampm
|
text: clockText.ampm
|
||||||
font.pixelSize: 120
|
font.pixelSize: 120
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: "white"
|
color: "white"
|
||||||
visible: clockText.ampm !== ""
|
visible: clockText.ampm !== ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: dateText
|
id: dateText
|
||||||
@@ -1355,7 +1363,7 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 38
|
Layout.preferredHeight: root.authFeedbackMessage !== "" ? 38 : 0
|
||||||
Layout.topMargin: -Theme.spacingS
|
Layout.topMargin: -Theme.spacingS
|
||||||
Layout.bottomMargin: -Theme.spacingS
|
Layout.bottomMargin: -Theme.spacingS
|
||||||
text: root.authFeedbackMessage
|
text: root.authFeedbackMessage
|
||||||
@@ -1374,48 +1382,150 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
// Password-screen actions: Switch User + Auto-login toggle as one compact chip row
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Item {
|
||||||
Layout.topMargin: 0
|
id: passwordActions
|
||||||
Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2
|
|
||||||
Layout.preferredHeight: 40
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
opacity: GreeterState.showPasswordInput ? 1 : 0
|
|
||||||
enabled: GreeterState.showPasswordInput
|
|
||||||
|
|
||||||
Behavior on opacity {
|
readonly property bool autoLoginAvailable: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
Layout.fillWidth: true
|
||||||
easing.type: Theme.standardEasing
|
Layout.topMargin: Theme.spacingXS
|
||||||
}
|
Layout.preferredHeight: visible ? 32 : 0
|
||||||
}
|
visible: GreeterState.showPasswordInput && !GreeterState.unlocking && (root.multipleUsersAvailable || autoLoginAvailable)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: switchUserRow
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
Rectangle {
|
||||||
name: "people"
|
id: switchUserChip
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.surfaceText
|
visible: root.multipleUsersAvailable
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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 {
|
Rectangle {
|
||||||
text: I18n.tr("Switch User")
|
id: autoLoginChip
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
visible: passwordActions.autoLoginAvailable
|
||||||
stateColor: Theme.primary
|
height: 32
|
||||||
cornerRadius: parent.radius
|
width: autoLoginContent.implicitWidth + Theme.spacingM * 2
|
||||||
enabled: !GreeterState.unlocking && GreeterState.showPasswordInput
|
radius: height / 2
|
||||||
onClicked: root.returnToUserPicker()
|
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();
|
launchTimeout.restart();
|
||||||
if (GreetdSettings.rememberLastSession) {
|
if (GreetdSettings.rememberLastSession) {
|
||||||
GreetdMemory.setLastSessionId(sessionPath);
|
GreetdMemory.setLastSessionId(sessionPath);
|
||||||
} else if (GreetdMemory.lastSessionId) {
|
GreetdMemory.setLastSessionExec(sessionCmd);
|
||||||
|
} else if (GreetdMemory.lastSessionId || GreetdMemory.lastSessionExec) {
|
||||||
GreetdMemory.setLastSessionId("");
|
GreetdMemory.setLastSessionId("");
|
||||||
}
|
}
|
||||||
if (GreetdSettings.rememberLastUser) {
|
if (GreetdSettings.rememberLastUser) {
|
||||||
@@ -1992,6 +2103,8 @@ Item {
|
|||||||
} else if (GreetdMemory.lastSuccessfulUser) {
|
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||||
GreetdMemory.setLastSuccessfulUser("");
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
}
|
}
|
||||||
|
if (root.autoLoginOnSuccess)
|
||||||
|
greeterAutoLoginPendingProcess.running = true;
|
||||||
pendingLaunchCommand = sessionCmd;
|
pendingLaunchCommand = sessionCmd;
|
||||||
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
||||||
memoryFlushTimer.restart();
|
memoryFlushTimer.restart();
|
||||||
|
|||||||
@@ -743,6 +743,16 @@ Item {
|
|||||||
checked: SettingsData.greeterRememberLastUser
|
checked: SettingsData.greeterRememberLastUser
|
||||||
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
|
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 {
|
SettingsCard {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ PanelWindow {
|
|||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
color: "transparent"
|
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)
|
readonly property real toastHeight: Theme.px(toastContent.height + Theme.spacingL * 2, dpr)
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
@@ -208,7 +208,7 @@ PanelWindow {
|
|||||||
buttonSize: Theme.iconSize + 8
|
buttonSize: Theme.iconSize + 8
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: ToastService.hasDetails || ToastService.currentLevel === ToastService.levelError
|
visible: ToastService.hasDetails || ToastService.currentLevel === ToastService.levelError || ToastService.isStickyCategory(ToastService.currentCategory)
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ToastService.hideToast();
|
ToastService.hideToast();
|
||||||
@@ -400,7 +400,7 @@ PanelWindow {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !ToastService.hasDetails
|
visible: !ToastService.hasDetails && !ToastService.isStickyCategory(ToastService.currentCategory)
|
||||||
onClicked: ToastService.hideToast()
|
onClicked: ToastService.hideToast()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ Singleton {
|
|||||||
property var lastErrorTime: ({})
|
property var lastErrorTime: ({})
|
||||||
property int errorThrottleMs: 1000
|
property int errorThrottleMs: 1000
|
||||||
property string currentCategory: ""
|
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 = "") {
|
function showToast(message, level = levelInfo, details = "", command = "", category = "") {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -137,7 +142,9 @@ Singleton {
|
|||||||
toastVisible = true
|
toastVisible = true
|
||||||
resetToastState()
|
resetToastState()
|
||||||
|
|
||||||
if (toast.level === levelError && hasDetails) {
|
if (isStickyCategory(toast.category)) {
|
||||||
|
toastTimer.stop()
|
||||||
|
} else if (toast.level === levelError && hasDetails) {
|
||||||
toastTimer.interval = 8000
|
toastTimer.interval = 8000
|
||||||
toastTimer.start()
|
toastTimer.start()
|
||||||
} else {
|
} else {
|
||||||
@@ -153,6 +160,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restartTimer() {
|
function restartTimer() {
|
||||||
|
if (isStickyCategory(currentCategory)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (hasDetails && currentLevel === levelError) {
|
if (hasDetails && currentLevel === levelError) {
|
||||||
toastTimer.interval = 8000
|
toastTimer.interval = 8000
|
||||||
toastTimer.restart()
|
toastTimer.restart()
|
||||||
|
|||||||
Reference in New Issue
Block a user