1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-03 02:52:07 -04:00

Compare commits

...

4 Commits

Author SHA1 Message Date
purian23
1eca9b4c2c feat: Implement immutable DMS command policy
- Added pre-run checks for greeter and setup commands to enforce policy restrictions
- Created cli-policy.default.json to define blocked commands and user messages for immutable environments.
2026-03-05 23:08:27 -05:00
purian23
fe5bd42e25 greeter: New Greeter Settings UI & Sync fixes
- Add PAM Auth via GUI
- Added new sync flags
- Refactored cache directory management & many others
- Fix for wireplumber permissions
- Fix for polkit auth w/icon
- Add pam_fprintd timeout=5 to prevent 30s auth blocks when using password
2026-03-05 23:04:59 -05:00
purian23
32d16d0673 refactor(greeter): Update auth flows and add configurable opts
- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
2026-03-04 14:17:56 -05:00
Lucas
27c26d35ab flake: allow extra QT packages in dms-shell package (#1903) 2026-03-03 21:47:45 -05:00
27 changed files with 3125 additions and 461 deletions

View File

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

View File

@@ -24,9 +24,10 @@ var greeterCmd = &cobra.Command{
} }
var greeterInstallCmd = &cobra.Command{ var greeterInstallCmd = &cobra.Command{
Use: "install", Use: "install",
Short: "Install and configure DMS greeter", Short: "Install and configure DMS greeter",
Long: "Install greetd and configure it to use DMS as the greeter interface", Long: "Install greetd and configure it to use DMS as the greeter interface",
PreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := installGreeter(); err != nil { if err := installGreeter(); err != nil {
log.Fatalf("Error installing greeter: %v", err) log.Fatalf("Error installing greeter: %v", err)
@@ -35,20 +36,39 @@ var greeterInstallCmd = &cobra.Command{
} }
var greeterSyncCmd = &cobra.Command{ var greeterSyncCmd = &cobra.Command{
Use: "sync", Use: "sync",
Short: "Sync DMS theme and settings with greeter", Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen", Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
PreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := syncGreeter(); err != nil { yes, _ := cmd.Flags().GetBool("yes")
auth, _ := cmd.Flags().GetBool("auth")
local, _ := cmd.Flags().GetBool("local")
term, _ := cmd.Flags().GetBool("terminal")
if term {
if err := syncInTerminal(yes, auth, local); err != nil {
log.Fatalf("Error launching sync in terminal: %v", err)
}
return
}
if err := syncGreeter(yes, auth, local); err != nil {
log.Fatalf("Error syncing greeter: %v", err) log.Fatalf("Error syncing greeter: %v", err)
} }
}, },
} }
func init() {
greeterSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts, use defaults (for UI)")
greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done")
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")
}
var greeterEnableCmd = &cobra.Command{ var greeterEnableCmd = &cobra.Command{
Use: "enable", Use: "enable",
Short: "Enable DMS greeter in greetd config", Short: "Enable DMS greeter in greetd config",
Long: "Configure greetd to use DMS as the greeter", Long: "Configure greetd to use DMS as the greeter",
PreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := enableGreeter(); err != nil { if err := enableGreeter(); err != nil {
log.Fatalf("Error enabling greeter: %v", err) log.Fatalf("Error enabling greeter: %v", err)
@@ -147,7 +167,7 @@ func installGreeter() error {
} }
fmt.Println("\nSynchronizing DMS configurations...") fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, ""); err != nil { if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil {
return err return err
} }
@@ -171,22 +191,88 @@ func installGreeter() error {
return nil return nil
} }
func syncGreeter() error { func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
fmt.Println("=== DMS Greeter Theme Sync ===") syncFlags := make([]string, 0, 3)
fmt.Println() if nonInteractive {
syncFlags = append(syncFlags, "--yes")
}
if forceAuth {
syncFlags = append(syncFlags, "--auth")
}
if local {
syncFlags = append(syncFlags, "--local")
}
shellSyncCmd := "dms greeter sync"
if len(syncFlags) > 0 {
shellSyncCmd += " " + strings.Join(syncFlags, " ")
}
shellCmd := shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
terminals := []struct {
name string
args []string
}{
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
{"kitty", []string{"bash", "-c", shellCmd}},
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
}
for _, t := range terminals {
if _, err := exec.LookPath(t.name); err != nil {
continue
}
cmd := exec.Command(t.name, t.args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
continue
}
_ = cmd.Process.Release()
return nil
}
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
if !nonInteractive {
fmt.Println("=== DMS Greeter Theme Sync ===")
fmt.Println()
}
logFunc := func(msg string) { logFunc := func(msg string) {
fmt.Println(msg) fmt.Println(msg)
} }
fmt.Println("Detecting DMS installation...") if !nonInteractive {
dmsPath, err := greeter.DetectDMSPath() fmt.Println("Detecting DMS installation...")
if err != nil { }
return err var dmsPath string
var err error
if local {
dmsPath, err = resolveLocalDMSPath()
if err != nil {
return err
}
if !nonInteractive {
fmt.Printf("✓ Using local DMS path: %s\n", dmsPath)
}
} else {
dmsPath, err = greeter.DetectDMSPath()
if err != nil {
return err
}
if !nonInteractive {
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
}
} }
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
if !isGreeterEnabled() { if !isGreeterEnabled() {
if nonInteractive {
return fmt.Errorf("greeter is not enabled; run 'dms greeter install' or 'dms greeter enable' first")
}
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.") fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
fmt.Print("Would you like to enable it now? (Y/n): ") fmt.Print("Would you like to enable it now? (Y/n): ")
@@ -203,9 +289,12 @@ func syncGreeter() error {
} }
} }
cacheDir := "/var/cache/dms-greeter" cacheDir := greeter.GreeterCacheDir
if _, err := os.Stat(cacheDir); os.IsNotExist(err) { if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir) logFunc("Cache directory not found — attempting to create it...")
if createErr := greeter.EnsureGreeterCacheDir(logFunc, ""); createErr != nil {
return fmt.Errorf("greeter cache directory not found at %s and could not be created: %w\nRun: sudo mkdir -p %s && sudo chown greeter:greeter %s", cacheDir, createErr, cacheDir, cacheDir)
}
} }
greeterGroup := greeter.DetectGreeterGroup() greeterGroup := greeter.DetectGreeterGroup()
@@ -224,6 +313,9 @@ func syncGreeter() error {
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup) inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
if !inGreeterGroup { if !inGreeterGroup {
if nonInteractive {
return fmt.Errorf("user must be in the %s group; run 'dms greeter sync' from a terminal to add", greeterGroup)
}
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup) fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup) fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
@@ -255,8 +347,14 @@ func syncGreeter() error {
return fmt.Errorf("no supported compositors found") return fmt.Errorf("no supported compositors found")
case 1: case 1:
compositor = compositors[0] compositor = compositors[0]
fmt.Printf("✓ Using compositor: %s\n", compositor) if !nonInteractive {
fmt.Printf("✓ Using compositor: %s\n", compositor)
}
default: default:
if nonInteractive {
compositor = compositors[0]
break
}
var err error var err error
compositor, err = promptCompositorChoice(compositors) compositor, err = promptCompositorChoice(compositors)
if err != nil { if err != nil {
@@ -264,27 +362,159 @@ func syncGreeter() error {
} }
fmt.Printf("✓ Selected compositor: %s\n", compositor) fmt.Printf("✓ Selected compositor: %s\n", compositor)
} }
} else { } else if !nonInteractive {
fmt.Printf("✓ Detected compositor from config: %s\n", compositor) fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
} }
if local {
localWrapperScript := filepath.Join(dmsPath, "Modules", "Greetd", "assets", "dms-greeter")
restoreWrapperOverride := func() {}
if info, statErr := os.Stat(localWrapperScript); statErr == nil && !info.IsDir() {
previousWrapperOverride, hadWrapperOverride := os.LookupEnv("DMS_GREETER_WRAPPER_CMD")
wrapperCmdOverride := "/usr/bin/bash " + localWrapperScript
_ = os.Setenv("DMS_GREETER_WRAPPER_CMD", wrapperCmdOverride)
restoreWrapperOverride = func() {
if hadWrapperOverride {
_ = os.Setenv("DMS_GREETER_WRAPPER_CMD", previousWrapperOverride)
} else {
_ = os.Unsetenv("DMS_GREETER_WRAPPER_CMD")
}
}
if !nonInteractive {
fmt.Printf("✓ Using local greeter wrapper script: %s\n", localWrapperScript)
}
} else if !nonInteractive {
fmt.Printf(" Local wrapper script not found at %s; using system wrapper.\n", localWrapperScript)
}
fmt.Println("\nUpdating greetd command to use local DMS path...")
err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, "")
restoreWrapperOverride()
if err != nil {
return fmt.Errorf("failed to apply local greeter path: %w", err)
}
if !nonInteractive {
fmt.Println(" Local mode applies both DMS path override (-p) and local wrapper behavior when available.")
}
} else {
greeterPathForConfig := ""
if !greeter.IsGreeterPackaged() {
greeterPathForConfig = dmsPath
}
fmt.Println("\nUpdating greetd command...")
if err := greeter.ConfigureGreetd(greeterPathForConfig, compositor, logFunc, ""); err != nil {
return fmt.Errorf("failed to update greetd command: %w", err)
}
}
fmt.Println("\nSetting up permissions and ACLs...") fmt.Println("\nSetting up permissions and ACLs...")
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
return err return err
} }
fmt.Println("\nSynchronizing DMS configurations...") fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, ""); err != nil { if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
return err return err
} }
fmt.Println("\n=== Sync Complete ===") fmt.Println("\n=== Sync Complete ===")
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.") fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
if forceAuth {
fmt.Println("PAM has been configured for fingerprint and U2F (where modules exist).")
}
fmt.Println("The changes will be visible on the next login screen.") fmt.Println("The changes will be visible on the next login screen.")
return nil return nil
} }
func hasDmsShellQml(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "shell.qml"))
return err == nil && !info.IsDir()
}
func resolveDMSLocalCandidate(path string) (string, bool) {
if path == "" {
return "", false
}
if hasDmsShellQml(path) {
abs, err := filepath.Abs(path)
if err != nil {
return path, true
}
return abs, true
}
quickshellPath := filepath.Join(path, "quickshell")
if hasDmsShellQml(quickshellPath) {
abs, err := filepath.Abs(quickshellPath)
if err != nil {
return quickshellPath, true
}
return abs, true
}
return "", false
}
func resolveLocalDMSPath() (string, error) {
if override := strings.TrimSpace(os.Getenv("DMS_LOCAL_PATH")); override != "" {
if resolved, ok := resolveDMSLocalCandidate(override); ok {
return resolved, nil
}
return "", fmt.Errorf("DMS_LOCAL_PATH is set but does not point to a valid DMS quickshell path: %s", override)
}
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get current directory: %w", err)
}
dir := wd
for {
if resolved, ok := resolveDMSLocalCandidate(dir); ok {
return resolved, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
homeDir, err := os.UserHomeDir()
if err == nil && homeDir != "" {
for _, candidate := range []string{
filepath.Join(homeDir, "dms"),
filepath.Join(homeDir, "DankMaterialShell"),
filepath.Join(homeDir, "dankmaterialshell"),
filepath.Join(homeDir, "projects", "dms"),
filepath.Join(homeDir, "src", "dms"),
} {
if resolved, ok := resolveDMSLocalCandidate(candidate); ok {
return resolved, nil
}
}
if entries, readErr := os.ReadDir(homeDir); readErr == nil {
for _, entry := range entries {
if !entry.IsDir() {
continue
}
name := strings.ToLower(entry.Name())
if !strings.Contains(name, "dms") && !strings.Contains(name, "dank") {
continue
}
if resolved, ok := resolveDMSLocalCandidate(filepath.Join(homeDir, entry.Name())); ok {
return resolved, nil
}
}
}
}
return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root or set DMS_LOCAL_PATH=/absolute/path/to/repo", wd)
}
func disableDisplayManager(dmName string) (bool, error) { func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName) state, err := getSystemdServiceState(dmName)
if err != nil { if err != nil {
@@ -486,22 +716,30 @@ func enableGreeter() error {
configPath := "/etc/greetd/config.toml" configPath := "/etc/greetd/config.toml"
if _, err := os.Stat(configPath); os.IsNotExist(err) { if _, err := os.Stat(configPath); os.IsNotExist(err) {
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath) return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
} else if err != nil {
return fmt.Errorf("failed to access greetd config at %s: %w", configPath, err)
} }
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read greetd config: %w", err)
}
configContent := string(data)
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
} }
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter") configAlreadyCorrect := isGreeterEnabled()
configuredCompositor := detectConfiguredCompositor()
logFunc := func(msg string) {
fmt.Println(msg)
}
if configAlreadyCorrect { if configAlreadyCorrect {
fmt.Println("✓ Greeter is already configured with dms-greeter") fmt.Println("✓ Greeter is already configured with dms-greeter")
if configuredCompositor != "" {
fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor)
}
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
}
if err := ensureGraphicalTarget(); err != nil { if err := ensureGraphicalTarget(); err != nil {
return err return err
@@ -548,68 +786,22 @@ func enableGreeter() error {
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor) fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
} }
backupPath := configPath + ".backup" greeterPathForConfig := ""
backupCmd := exec.Command("sudo", "cp", configPath, backupPath) if !greeter.IsGreeterPackaged() {
if err := backupCmd.Run(); err != nil { dmsPath, err := greeter.DetectDMSPath()
return fmt.Errorf("failed to backup config: %w", err) if err != nil {
} return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err)
fmt.Printf("✓ Backed up config to %s\n", backupPath)
lines := strings.Split(configContent, "\n")
var newLines []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
newLines = append(newLines, line)
} }
greeterPathForConfig = dmsPath
}
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
return fmt.Errorf("failed to configure greetd: %w", err)
} }
wrapperCmd, err := findCommandPath("dms-greeter") if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
if err != nil { fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
} }
compositorLower := strings.ToLower(selectedCompositor)
commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
var finalLines []string
inDefaultSession := false
commandAdded := false
for _, line := range newLines {
finalLines = append(finalLines, line)
trimmed := strings.TrimSpace(line)
if trimmed == "[default_session]" {
inDefaultSession = true
}
if inDefaultSession && !commandAdded {
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
finalLines = append(finalLines, commandLine)
commandAdded = true
}
}
}
if !commandAdded {
finalLines = append(finalLines, commandLine)
}
newConfig := strings.Join(finalLines, "\n")
tmpFile := "/tmp/greetd-config.toml"
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
return fmt.Errorf("failed to write temp config: %w", err)
}
moveCmd := exec.Command("sudo", "mv", tmpFile, configPath)
if err := moveCmd.Run(); err != nil {
return fmt.Errorf("failed to update config: %w", err)
}
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
if err := ensureGraphicalTarget(); err != nil { if err := ensureGraphicalTarget(); err != nil {
return err return err
} }
@@ -631,38 +823,164 @@ func enableGreeter() error {
} }
func isGreeterEnabled() bool { func isGreeterEnabled() bool {
data, err := os.ReadFile("/etc/greetd/config.toml") command := readDefaultSessionCommand("/etc/greetd/config.toml")
if err != nil { return command != "" && strings.Contains(command, "dms-greeter")
return false
}
return strings.Contains(string(data), "dms-greeter")
} }
func detectConfiguredCompositor() string { func detectConfiguredCompositor() string {
data, err := os.ReadFile("/etc/greetd/config.toml") command := strings.ToLower(readDefaultSessionCommand("/etc/greetd/config.toml"))
switch {
case strings.Contains(command, "--command niri"):
return "niri"
case strings.Contains(command, "--command hyprland"):
return "hyprland"
case strings.Contains(command, "--command sway"):
return "sway"
}
return ""
}
func stripTomlComment(line string) string {
trimmed := strings.TrimSpace(line)
if idx := strings.Index(trimmed, "#"); idx >= 0 {
return strings.TrimSpace(trimmed[:idx])
}
return trimmed
}
func parseTomlSection(line string) (string, bool) {
trimmed := stripTomlComment(line)
if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
return "", false
}
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
}
func readDefaultSessionCommand(configPath string) string {
data, err := os.ReadFile(configPath)
if err != nil { if err != nil {
return "" return ""
} }
for _, line := range strings.Split(string(data), "\n") { inDefaultSession := false
trimmed := strings.TrimSpace(line) for line := range strings.SplitSeq(string(data), "\n") {
if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") { if section, ok := parseTomlSection(line); ok {
inDefaultSession = section == "default_session"
continue continue
} }
switch { if !inDefaultSession {
case strings.Contains(trimmed, "--command niri"): continue
return "niri" }
case strings.Contains(trimmed, "--command hyprland"):
return "hyprland" trimmed := stripTomlComment(line)
case strings.Contains(trimmed, "--command sway"): if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
return "sway" continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
} }
} }
return "" return ""
} }
func extractGreeterCacheDirFromCommand(command string) string {
if command == "" {
return greeter.GreeterCacheDir
}
tokens := strings.Fields(command)
for i := 0; i < len(tokens); i++ {
token := strings.Trim(tokens[i], "\"")
if token == "--cache-dir" && i+1 < len(tokens) {
return strings.Trim(tokens[i+1], "\"")
}
if strings.HasPrefix(token, "--cache-dir=") {
value := strings.TrimPrefix(token, "--cache-dir=")
value = strings.Trim(value, "\"")
if value != "" {
return value
}
}
}
return greeter.GreeterCacheDir
}
func extractGreeterWrapperFromCommand(command string) string {
if command == "" {
return ""
}
tokens := strings.Fields(command)
if len(tokens) == 0 {
return ""
}
return strings.Trim(tokens[0], "\"")
}
func extractGreeterPathOverrideFromCommand(command string) string {
if command == "" {
return ""
}
tokens := strings.Fields(command)
for i := 0; i < len(tokens); i++ {
token := strings.Trim(tokens[i], "\"")
if (token == "-p" || token == "--path") && i+1 < len(tokens) {
return strings.Trim(tokens[i+1], "\"")
}
if strings.HasPrefix(token, "--path=") {
value := strings.TrimPrefix(token, "--path=")
value = strings.Trim(value, "\"")
if value != "" {
return value
}
}
}
return ""
}
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
if pamText == "" {
return false, false, false, false
}
lines := strings.Split(pamText, "\n")
inManaged := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch trimmed {
case greeter.GreeterPamManagedBlockStart:
managed = true
inManaged = true
continue
case greeter.GreeterPamManagedBlockEnd:
inManaged = false
continue
}
if strings.HasPrefix(trimmed, "# DMS greeter fingerprint") || strings.HasPrefix(trimmed, "# DMS greeter U2F") {
legacy = true
}
if !inManaged {
continue
}
if strings.Contains(trimmed, "pam_fprintd") {
fingerprint = true
}
if strings.Contains(trimmed, "pam_u2f") {
u2f = true
}
}
return managed, fingerprint, u2f, legacy
}
func packageInstallHint() string { func packageInstallHint() string {
osInfo, err := distros.GetOSInfo() osInfo, err := distros.GetOSInfo()
if err != nil { if err != nil {
@@ -741,39 +1059,40 @@ func checkGreeterStatus() error {
} }
configPath := "/etc/greetd/config.toml" configPath := "/etc/greetd/config.toml"
configuredCommand := ""
allGood := true
fmt.Println("Greeter Configuration:") fmt.Println("Greeter Configuration:")
if data, err := os.ReadFile(configPath); err == nil { if _, err := os.ReadFile(configPath); err == nil {
configContent := string(data) configuredCommand = readDefaultSessionCommand(configPath)
if strings.Contains(configContent, "dms-greeter") { if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") {
lines := strings.SplitSeq(configContent, "\n") fmt.Println(" ✓ Greeter is enabled")
for line := range lines { if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
trimmed := strings.TrimSpace(line) fmt.Printf(" Wrapper: %s\n", wrapper)
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") { }
parts := strings.SplitN(trimmed, "=", 2) if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
if len(parts) == 2 { fmt.Printf(" DMS path override: %s\n", pathOverride)
command := strings.Trim(strings.TrimSpace(parts[1]), `"`) }
fmt.Println(" ✓ Greeter is enabled")
if strings.Contains(command, "--command niri") { compositor := detectConfiguredCompositor()
fmt.Println(" Compositor: niri") switch compositor {
} else if strings.Contains(command, "--command hyprland") { case "niri":
fmt.Println(" Compositor: Hyprland") fmt.Println(" Compositor: niri")
} else if strings.Contains(command, "--command sway") { case "hyprland":
fmt.Println(" Compositor: sway") fmt.Println(" Compositor: Hyprland")
} else { case "sway":
fmt.Println(" Compositor: unknown") fmt.Println(" Compositor: sway")
} default:
} fmt.Println(" Compositor: unknown")
break
}
} }
} else { } else {
fmt.Println(" ✗ Greeter is NOT enabled") fmt.Println(" ✗ Greeter is NOT enabled")
fmt.Println(" Run 'dms greeter enable' to enable it") fmt.Println(" Run 'dms greeter enable' to enable it")
allGood = false
} }
} else { } else {
fmt.Println(" ✗ Greeter config not found") fmt.Println(" ✗ Greeter config not found")
fmt.Printf(" %s\n", packageInstallHint()) fmt.Printf(" %s\n", packageInstallHint())
allGood = false
} }
fmt.Println("\nGroup Membership:") fmt.Println("\nGroup Membership:")
@@ -792,8 +1111,12 @@ func checkGreeterStatus() error {
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions") fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
} }
cacheDir := "/var/cache/dms-greeter" cacheDir := extractGreeterCacheDirFromCommand(configuredCommand)
fmt.Println("\nGreeter Cache Directory:") fmt.Println("\nGreeter Cache Directory:")
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
if cacheDir != greeter.GreeterCacheDir {
fmt.Printf(" ⚠ Non-default cache dir detected (default: %s)\n", greeter.GreeterCacheDir)
}
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() { if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
fmt.Printf(" ✓ %s exists\n", cacheDir) fmt.Printf(" ✓ %s exists\n", cacheDir)
} else { } else {
@@ -825,7 +1148,6 @@ func checkGreeterStatus() error {
}, },
} }
allGood := true
for _, link := range symlinks { for _, link := range symlinks {
targetInfo, err := os.Lstat(link.target) targetInfo, err := os.Lstat(link.target)
if err != nil { if err != nil {
@@ -864,11 +1186,80 @@ func checkGreeterStatus() error {
fmt.Printf(" ✓ %s: synced correctly\n", link.desc) fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
} }
fmt.Println("\nGreeter Wallpaper Override:")
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
fmt.Printf(" ✓ Override file present: %s\n", overridePath)
} else if os.IsNotExist(err) {
fmt.Println(" Override file not present (desktop/session wallpaper fallback in effect)")
} else if err != nil {
fmt.Printf(" ✗ Could not inspect override file: %v\n", err)
allGood = false
} else {
fmt.Printf(" ✗ Override path is not a regular file: %s\n", overridePath)
allGood = false
}
fmt.Println("\nGreeter PAM Authentication (DMS-managed block):")
if greeter.IsNixOS() {
fmt.Println(" NixOS detected: PAM is managed by NixOS modules.")
fmt.Println(" Configure fingerprint/U2F via your greetd NixOS module (security.pam.services.greetd).")
fmt.Println()
if allGood && inGreeterGroup {
fmt.Println("✓ All checks passed! Greeter is properly configured.")
} else if !allGood {
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.")
} else if !inGreeterGroup {
fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup)
}
return nil
}
greetdPamPath := "/etc/pam.d/greetd"
pamData, err := os.ReadFile(greetdPamPath)
if err != nil {
fmt.Printf(" ✗ Failed to read %s: %v\n", greetdPamPath, err)
allGood = false
} else {
managed, managedFprint, managedU2f, legacyManaged := parseManagedGreeterPamAuth(string(pamData))
if managed {
fmt.Println(" ✓ Managed auth block present")
if managedFprint {
fmt.Println(" - fingerprint: enabled")
} else {
fmt.Println(" - fingerprint: disabled")
}
if managedU2f {
fmt.Println(" - security key (U2F): enabled")
} else {
fmt.Println(" - security key (U2F): disabled")
}
} else {
fmt.Println(" No managed auth block present (fingerprint/U2F disabled for greeter)")
}
if legacyManaged {
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
allGood = false
}
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
if managedFprint {
if includedFprintFile != "" {
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
allGood = false
}
} else if includedFprintFile != "" {
fmt.Printf(" Fingerprint auth is enabled via included %s.\n", includedFprintFile)
fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.")
}
}
fmt.Println() fmt.Println()
if allGood && inGreeterGroup { if allGood && inGreeterGroup {
fmt.Println("✓ All checks passed! Greeter is properly configured.") fmt.Println("✓ All checks passed! Greeter is properly configured.")
} else if !allGood { } else if !allGood {
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to fix symlinks.") fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.")
} else if !inGreeterGroup {
fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup)
} }
return nil return nil

View File

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

View File

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

View File

@@ -7,14 +7,6 @@ import (
"strings" "strings"
) )
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
}
return path, nil
}
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName) cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run() err := cmd.Run()

View File

@@ -3,6 +3,7 @@ package greeter
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -17,10 +18,29 @@ import (
"github.com/sblinch/kdl-go/document" "github.com/sblinch/kdl-go/document"
) )
const (
GreeterCacheDir = "/var/cache/dms-greeter"
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
)
var includedPamAuthFiles = []string{"system-auth", "common-auth", "password-auth"}
func DetectDMSPath() (string, error) { func DetectDMSPath() (string, error) {
return config.LocateDMSConfig() return config.LocateDMSConfig()
} }
// IsNixOS returns true when running on NixOS, which manages PAM configs through
// its module system. The DMS PAM managed block must not be written on NixOS.
func IsNixOS() bool {
_, err := os.Stat("/etc/NIXOS")
return err == nil
}
func DetectGreeterGroup() string { func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group") data, err := os.ReadFile("/etc/group")
if err != nil { if err != nil {
@@ -36,6 +56,196 @@ func DetectGreeterGroup() string {
return "greeter" return "greeter"
} }
func hasPasswdUser(passwdData, user string) bool {
prefix := user + ":"
for line := range strings.SplitSeq(passwdData, "\n") {
if strings.HasPrefix(line, prefix) {
return true
}
}
return false
}
func findPasswdUser(passwdData string, candidates ...string) (string, bool) {
for _, candidate := range candidates {
if hasPasswdUser(passwdData, candidate) {
return candidate, true
}
}
return "", false
}
func stripTomlComment(line string) string {
trimmed := strings.TrimSpace(line)
if idx := strings.Index(trimmed, "#"); idx >= 0 {
return strings.TrimSpace(trimmed[:idx])
}
return trimmed
}
func parseTomlSection(line string) (string, bool) {
trimmed := stripTomlComment(line)
if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
return "", false
}
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
}
func extractDefaultSessionUser(configContent string) string {
inDefaultSession := false
for line := range strings.SplitSeq(configContent, "\n") {
if section, ok := parseTomlSection(line); ok {
inDefaultSession = section == "default_session"
continue
}
if !inDefaultSession {
continue
}
trimmed := stripTomlComment(line)
if !strings.HasPrefix(trimmed, "user =") && !strings.HasPrefix(trimmed, "user=") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
user := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if user != "" {
return user
}
}
return ""
}
func upsertDefaultSession(configContent, greeterUser, command string) string {
lines := strings.Split(configContent, "\n")
var out []string
inDefaultSession := false
foundDefaultSession := false
defaultSessionUserSet := false
defaultSessionCommandSet := false
appendDefaultSessionFields := func() {
if !defaultSessionUserSet {
out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser))
}
if !defaultSessionCommandSet {
out = append(out, command)
}
}
for _, line := range lines {
if section, ok := parseTomlSection(line); ok {
if inDefaultSession {
appendDefaultSessionFields()
}
inDefaultSession = section == "default_session"
if inDefaultSession {
foundDefaultSession = true
defaultSessionUserSet = false
defaultSessionCommandSet = false
}
out = append(out, line)
continue
}
if inDefaultSession {
trimmed := stripTomlComment(line)
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser))
defaultSessionUserSet = true
continue
}
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
if !defaultSessionCommandSet {
out = append(out, command)
defaultSessionCommandSet = true
}
continue
}
}
out = append(out, line)
}
if inDefaultSession {
appendDefaultSessionFields()
}
if !foundDefaultSession {
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
out = append(out, "")
}
out = append(out, "[default_session]")
out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser))
out = append(out, command)
}
return strings.Join(out, "\n")
}
func DetectGreeterUser() string {
passwdData, err := os.ReadFile("/etc/passwd")
if err == nil {
passwdContent := string(passwdData)
if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil {
if configured := extractDefaultSessionUser(string(configData)); configured != "" && hasPasswdUser(passwdContent, configured) {
return configured
}
}
if user, found := findPasswdUser(passwdContent, "greeter", "_greeter", "greetd"); found {
return user
}
} else {
fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/passwd, defaulting greeter user to 'greeter'")
}
if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil {
if configured := extractDefaultSessionUser(string(configData)); configured != "" {
return configured
}
}
fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter user found, defaulting to 'greeter'")
return "greeter"
}
func resolveGreeterWrapperPath() string {
if override := strings.TrimSpace(os.Getenv("DMS_GREETER_WRAPPER_CMD")); override != "" {
return override
}
for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} {
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
return candidate
}
}
if path, err := exec.LookPath("dms-greeter"); err == nil {
resolved := path
if realPath, realErr := filepath.EvalSymlinks(path); realErr == nil {
resolved = realPath
}
if strings.HasPrefix(resolved, "/home/") || strings.HasPrefix(resolved, "/tmp/") {
fmt.Fprintf(os.Stderr, "⚠ Warning: ignoring non-system dms-greeter on PATH: %s\n", path)
} else {
return path
}
}
return "/usr/bin/dms-greeter"
}
func DetectCompositors() []string { func DetectCompositors() []string {
var compositors []string var compositors []string
@@ -289,9 +499,13 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if utils.CommandExists("dms-greeter") { if IsGreeterPackaged() {
logFunc("✓ dms-greeter wrapper already installed") logFunc("✓ dms-greeter package already installed")
} else { } else {
if dmsPath == "" {
return fmt.Errorf("dms path is required for manual dms-greeter wrapper installs")
}
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets") assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
wrapperSrc := filepath.Join(assetsDir, "dms-greeter") wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
@@ -300,10 +514,14 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
} }
wrapperDst := "/usr/local/bin/dms-greeter" wrapperDst := "/usr/local/bin/dms-greeter"
action := "Installed"
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
action = "Updated"
}
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil { if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err) return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
} }
logFunc(fmt.Sprintf("✓ Installed dms-greeter wrapper to %s", wrapperDst)) logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil { if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
return fmt.Errorf("failed to make wrapper executable: %w", err) return fmt.Errorf("failed to make wrapper executable: %w", err)
@@ -328,7 +546,21 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
} }
} }
cacheDir := "/var/cache/dms-greeter" if err := EnsureGreeterCacheDir(logFunc, sudoPassword); err != nil {
return err
}
return nil
}
// EnsureGreeterCacheDir creates /var/cache/dms-greeter with correct ownership if it does not exist.
// It is safe to call multiple times (idempotent).
func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
cacheDir := GreeterCacheDir
if _, err := os.Stat(cacheDir); err == nil {
return nil
}
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil { if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err) return fmt.Errorf("failed to create cache directory: %w", err)
} }
@@ -340,11 +572,10 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
return fmt.Errorf("failed to set cache directory owner: %w", err) return fmt.Errorf("failed to set cache directory owner: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil { if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory permissions: %w", err) return fmt.Errorf("failed to set cache directory permissions: %w", err)
} }
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner)) logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, mode: 750)", cacheDir, owner))
return nil return nil
} }
@@ -544,13 +775,13 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
return nil return nil
} }
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string, forceAuth bool) error {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
} }
cacheDir := "/var/cache/dms-greeter" cacheDir := GreeterCacheDir
symlinks := []struct { symlinks := []struct {
source string source string
@@ -578,28 +809,33 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
sourceDir := filepath.Dir(link.source) sourceDir := filepath.Dir(link.source)
if _, err := os.Stat(sourceDir); os.IsNotExist(err) { if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
if err := os.MkdirAll(sourceDir, 0o755); err != nil { if err := os.MkdirAll(sourceDir, 0o755); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err)) return fmt.Errorf("failed to create source directory %s for %s: %w", sourceDir, link.desc, err)
continue
} }
} }
if _, err := os.Stat(link.source); os.IsNotExist(err) { if _, err := os.Stat(link.source); os.IsNotExist(err) {
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil { if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err)) return fmt.Errorf("failed to create source file %s for %s: %w", link.source, link.desc, err)
continue
} }
} }
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target) _ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil { if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err)) return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
continue
} }
logFunc(fmt.Sprintf("✓ Synced %s", link.desc)) logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
} }
if err := syncGreeterWallpaperOverride(homeDir, cacheDir, logFunc, sudoPassword); err != nil {
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
}
if err := syncGreeterPamConfig(homeDir, logFunc, sudoPassword, forceAuth); err != nil {
return fmt.Errorf("greeter PAM config sync failed: %w", err)
}
if strings.ToLower(compositor) != "niri" { if strings.ToLower(compositor) != "niri" {
return nil return nil
} }
@@ -611,6 +847,293 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
return nil return nil
} }
func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string), sudoPassword string) error {
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
}
var settings struct {
GreeterWallpaperPath string `json:"greeterWallpaperPath"`
}
if err := json.Unmarshal(data, &settings); err != nil {
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
}
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
if settings.GreeterWallpaperPath == "" {
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
}
logFunc("✓ Cleared greeter wallpaper override")
return nil
}
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
}
src := settings.GreeterWallpaperPath
if !filepath.IsAbs(src) {
src = filepath.Join(homeDir, src)
}
st, err := os.Stat(src)
if err != nil {
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err)
}
if st.IsDir() {
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
}
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil {
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
}
greeterGroup := DetectGreeterGroup()
if err := runSudoCmd(sudoPassword, "chown", "greeter:"+greeterGroup, destPath); err != nil {
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
}
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
}
logFunc("✓ Synced greeter wallpaper override")
return nil
}
func pamModuleExists(module string) bool {
for _, libDir := range []string{
"/usr/lib64/security",
"/usr/lib/security",
"/lib/x86_64-linux-gnu/security",
"/usr/lib/x86_64-linux-gnu/security",
"/usr/lib/aarch64-linux-gnu/security",
} {
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
return true
}
}
return false
}
func stripManagedGreeterPamBlock(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
inManagedBlock := false
removed := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == GreeterPamManagedBlockStart {
inManagedBlock = true
removed = true
continue
}
if trimmed == GreeterPamManagedBlockEnd {
inManagedBlock = false
removed = true
continue
}
if inManagedBlock {
removed = true
continue
}
filtered = append(filtered, line)
}
return strings.Join(filtered, "\n"), removed
}
func stripLegacyGreeterPamLines(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
removed := false
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
removed = true
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "auth") &&
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
i++
}
}
continue
}
filtered = append(filtered, lines[i])
}
return strings.Join(filtered, "\n"), removed
}
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
block := strings.Join(blockLines, "\n")
prefix := strings.Join(lines[:i], "\n")
suffix := strings.Join(lines[i:], "\n")
switch {
case prefix == "":
return block + "\n" + suffix, nil
case suffix == "":
return prefix + "\n" + block, nil
default:
return prefix + "\n" + block + "\n" + suffix, nil
}
}
}
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}
func PamTextIncludesFile(pamText, filename string) bool {
lines := strings.Split(pamText, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, filename) &&
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
return true
}
}
return false
}
func PamFileHasModule(pamFilePath, module string) bool {
data, err := os.ReadFile(pamFilePath)
if err != nil {
return false
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, module) {
return true
}
}
return false
}
func DetectIncludedPamModule(pamText, module string) string {
for _, includedFile := range includedPamAuthFiles {
if PamTextIncludesFile(pamText, includedFile) && PamFileHasModule("/etc/pam.d/"+includedFile, module) {
return includedFile
}
}
return ""
}
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
var wantFprint, wantU2f bool
if forceAuth {
wantFprint = pamModuleExists("pam_fprintd.so")
wantU2f = pamModuleExists("pam_u2f.so")
} else {
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
data = []byte("{}")
} else {
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
}
}
var settings struct {
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
}
if err := json.Unmarshal(data, &settings); err != nil {
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
}
fprintModule := pamModuleExists("pam_fprintd.so")
u2fModule := pamModuleExists("pam_u2f.so")
wantFprint = settings.GreeterEnableFprint && fprintModule
wantU2f = settings.GreeterEnableU2f && u2fModule
if settings.GreeterEnableFprint && !fprintModule {
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
}
if settings.GreeterEnableU2f && !u2fModule {
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
}
}
if IsNixOS() {
logFunc(" NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
return nil
}
greetdPamPath := "/etc/pam.d/greetd"
pamData, err := os.ReadFile(greetdPamPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
}
originalContent := string(pamData)
content, _ := stripManagedGreeterPamBlock(originalContent)
content, _ = stripLegacyGreeterPamLines(content)
includedFprintFile := DetectIncludedPamModule(content, "pam_fprintd.so")
if wantFprint && includedFprintFile != "" {
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
wantFprint = false
}
if !wantFprint && includedFprintFile != "" {
logFunc(" Fingerprint auth is still enabled via included " + includedFprintFile + ".")
logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.")
}
if wantFprint || wantU2f {
blockLines := []string{GreeterPamManagedBlockStart}
if wantFprint {
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
}
if wantU2f {
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
}
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
content, err = insertManagedGreeterPamBlock(content, blockLines, greetdPamPath)
if err != nil {
return err
}
}
if content == originalContent {
return nil
}
tmpFile, err := os.CreateTemp("", "greetd-pam-*.conf")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return err
}
if err := tmpFile.Close(); err != nil {
return err
}
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
}
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
}
if wantFprint || wantU2f {
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
} else {
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
}
return nil
}
type niriGreeterSync struct { type niriGreeterSync struct {
processed map[string]bool processed map[string]bool
nodes []*document.Node nodes []*document.Node
@@ -752,6 +1275,8 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
} }
// Strip existing -C or --config and their arguments // Strip existing -C or --config and their arguments
command = stripConfigFlag(command) command = stripConfigFlag(command)
command = stripCacheDirFlag(command)
command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir)
newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath) newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath)
idx := strings.Index(line, "command") idx := strings.Index(line, "command")
@@ -768,10 +1293,6 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
return nil return nil
} }
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") tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temp greetd config: %w", err) return fmt.Errorf("failed to create temp greetd config: %w", err)
@@ -802,7 +1323,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
} }
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405")) backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
return runSudoCmd(sudoPassword, "cp", "-p", path, backupPath) if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil {
return err
}
return runSudoCmd(sudoPassword, "chmod", "644", backupPath)
} }
func (s *niriGreeterSync) processFile(filePath string) error { func (s *niriGreeterSync) processFile(filePath string) error {
@@ -948,96 +1472,63 @@ func (s *niriGreeterSync) render() string {
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
configPath := "/etc/greetd/config.toml" configPath := "/etc/greetd/config.toml"
backupPath := fmt.Sprintf("%s.backup-%s", configPath, time.Now().Format("20060102-150405"))
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup greetd config: %w", err)
}
if _, err := os.Stat(configPath); err == nil { if _, err := os.Stat(configPath); err == nil {
backupPath := configPath + ".backup"
if err := runSudoCmd(sudoPassword, "cp", configPath, backupPath); err != nil {
return fmt.Errorf("failed to backup config: %w", err)
}
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath)) logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
} }
greeterUser := DetectGreeterGroup() greeterUser := DetectGreeterUser()
var configContent string var configContent string
if data, err := os.ReadFile(configPath); err == nil { if data, err := os.ReadFile(configPath); err == nil {
configContent = string(data) configContent = string(data)
} else { } else if os.IsNotExist(err) {
configContent = fmt.Sprintf(`[terminal] configContent = `[terminal]
vt = 1 vt = 1
[default_session] [default_session]
`
user = "%s" } else {
`, greeterUser) return fmt.Errorf("failed to read greetd config: %w", err)
} }
lines := strings.Split(configContent, "\n") wrapperCmd := resolveGreeterWrapperPath()
var newLines []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
} else {
newLines = append(newLines, line)
}
}
}
// If dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
wrapperCmd := "dms-greeter"
if !utils.CommandExists("dms-greeter") {
wrapperCmd = "/usr/local/bin/dms-greeter"
}
compositorLower := strings.ToLower(compositor) compositorLower := strings.ToLower(compositor)
var command string commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
if dmsPath == "" {
command = fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
} else {
command = fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
}
var finalLines []string
inDefaultSession := false
commandAdded := false
for _, line := range newLines {
finalLines = append(finalLines, line)
trimmed := strings.TrimSpace(line)
if trimmed == "[default_session]" {
inDefaultSession = true
}
if inDefaultSession && !commandAdded && trimmed != "" && !strings.HasPrefix(trimmed, "[") {
if !strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "user") {
finalLines = append(finalLines, command)
commandAdded = true
}
}
}
if !commandAdded {
finalLines = append(finalLines, command)
}
newConfig := strings.Join(finalLines, "\n")
tmpFile := "/tmp/greetd-config.toml"
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
return fmt.Errorf("failed to write temp config: %w", err)
}
if err := runSudoCmd(sudoPassword, "mv", tmpFile, configPath); err != nil {
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
}
cmdDesc := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower)
if dmsPath != "" { if dmsPath != "" {
cmdDesc = fmt.Sprintf("%s -p %s", cmdDesc, dmsPath) commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath)
} }
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, cmdDesc))
commandLine := fmt.Sprintf(`command = "%s"`, commandValue)
newConfig := upsertDefaultSession(configContent, greeterUser, commandLine)
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp greetd config: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(newConfig); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write temp greetd config: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp greetd config: %w", err)
}
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
return fmt.Errorf("failed to create /etc/greetd: %w", err)
}
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to install greetd config: %w", err)
}
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue))
return nil return nil
} }
@@ -1072,6 +1563,30 @@ func stripConfigFlag(command string) string {
return command return command
} }
func stripCacheDirFlag(command string) string {
fields := strings.Fields(command)
if len(fields) == 0 {
return strings.TrimSpace(command)
}
filtered := make([]string, 0, len(fields))
for i := 0; i < len(fields); i++ {
token := fields[i]
if token == "--cache-dir" {
if i+1 < len(fields) {
i++
}
continue
}
if strings.HasPrefix(token, "--cache-dir=") {
continue
}
filtered = append(filtered, token)
}
return strings.Join(filtered, " ")
}
// getDebianOBSSlug returns the OBS repository slug for the running Debian version. // getDebianOBSSlug returns the OBS repository slug for the running Debian version.
func getDebianOBSSlug(osInfo *distros.OSInfo) string { func getDebianOBSSlug(osInfo *distros.OSInfo) string {
versionID := strings.ToLower(osInfo.VersionID) versionID := strings.ToLower(osInfo.VersionID)
@@ -1248,7 +1763,7 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
} }
logFunc("Synchronizing DMS configurations...") logFunc("Synchronizing DMS configurations...")
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil { if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword, false); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err)) logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
} }

View File

@@ -1,6 +1,7 @@
package network package network
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) { func TestDetectNetworkStack_Integration(t *testing.T) {
result, err := DetectNetworkStack() result, err := DetectNetworkStack()
if err != nil && strings.Contains(err.Error(), "connect system bus") {
t.Skipf("system D-Bus unavailable: %v", err)
}
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, result) if assert.NotNil(t, result) {
assert.NotEmpty(t, result.ChosenReason) assert.NotEmpty(t, result.ChosenReason)
}
} }

118
flake.nix
View File

@@ -72,76 +72,82 @@
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ {
dms-shell = pkgs.buildGoModule ( dms-shell = pkgs.lib.makeOverridable (
let
rootSrc = ./.;
in
{ {
inherit version; extraQtPackages ? [ ],
pname = "dms-shell"; }:
src = ./core; pkgs.buildGoModule (
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ="; let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X 'main.Version=${version}'" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''
mkdir -p $out/share/quickshell/dms mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/ cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon # Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \ install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop $out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \ install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg $out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \ --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}" --prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \ substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh) --zsh <($out/bin/dms completion zsh)
''; '';
meta = { meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO"; description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com"; homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}"; changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
mainProgram = "dms"; mainProgram = "dms";
platforms = pkgs.lib.platforms.linux; platforms = pkgs.lib.platforms.linux;
}; };
} }
); )
) { };
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;

View File

@@ -22,8 +22,8 @@ Singleton {
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property var _loadedSessionSnapshot: null property var _loadedSessionSnapshot: null
readonly property var _hooks: ({ readonly property var _hooks: ({
"updateLocale": updateLocale "updateLocale": updateLocale
}) })
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -1245,7 +1245,7 @@ Singleton {
id: greeterSessionFile id: greeterSessionFile
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/session.json"; return greetCfgDir + "/session.json";
} }
preload: isGreeterMode preload: isGreeterMode

View File

@@ -313,6 +313,17 @@ Singleton {
property string centeringMode: "index" property string centeringMode: "index"
property string clockDateFormat: "" property string clockDateFormat: ""
property string lockDateFormat: "" property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property int mediaSize: 1 property int mediaSize: 1
property string appLauncherViewMode: "list" property string appLauncherViewMode: "list"
@@ -1155,7 +1166,7 @@ Singleton {
"updateCompositorLayout": updateCompositorLayout, "updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs, "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor, "updateCompositorCursor": updateCompositorCursor
}) })
function set(key, value) { function set(key, value) {

View File

@@ -1084,7 +1084,7 @@ Singleton {
property string fontFamily: { property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontFamily; return GreetdSettings.getEffectiveFontFamily();
} }
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable"; return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
} }
@@ -1987,7 +1987,7 @@ Singleton {
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json"; const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return colorsPath; return colorsPath;
} }

View File

@@ -52,7 +52,7 @@ Singleton {
} }
property var fprintdDetectionProcess: Process { property var fprintdDetectionProcess: Process {
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"] command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"${USER:-$(id -un)}\" >/dev/null 2>&1"]
running: false running: false
onExited: function (exitCode) { onExited: function (exitCode) {
if (!settingsRoot) if (!settingsRoot)

View File

@@ -164,6 +164,17 @@ var SPEC = {
centeringMode: { def: "index" }, centeringMode: { def: "index" },
clockDateFormat: { def: "" }, clockDateFormat: { def: "" },
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
greeterEnableFprint: { def: false },
greeterEnableU2f: { def: false },
greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false },
greeterPadHours12Hour: { def: false },
greeterLockDateFormat: { def: "" },
greeterFontFamily: { def: "" },
greeterWallpaperFillMode: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },

View File

@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -11,8 +12,45 @@ FloatingWindow {
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
property bool awaitingFprintForPassword: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property string polkitEtcPamText: ""
property string polkitLibPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
readonly property bool polkitPamHasFprint: {
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
if (!polkitText)
return false;
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
}
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (line && line.includes(moduleName))
return true;
}
return false;
}
function focusPasswordField() { function focusPasswordField() {
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
} }
@@ -20,6 +58,7 @@ FloatingWindow {
function show() { function show() {
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
visible = true; visible = true;
Qt.callLater(focusPasswordField); Qt.callLater(focusPasswordField);
} }
@@ -28,17 +67,27 @@ FloatingWindow {
visible = false; visible = false;
} }
function _commitSubmit() {
isLoading = true;
awaitingFprintForPassword = false;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function submitAuth() { function submitAuth() {
if (!currentFlow || isLoading) if (!currentFlow || isLoading)
return; return;
isLoading = true; if (!currentFlow.isResponseRequired) {
currentFlow.submit(passwordInput); awaitingFprintForPassword = true;
passwordInput = ""; return;
}
_commitSubmit();
} }
function cancelAuth() { function cancelAuth() {
if (isLoading) if (isLoading)
return; return;
awaitingFprintForPassword = false;
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest(); currentFlow.cancelAuthenticationRequest();
return; return;
@@ -60,6 +109,7 @@ FloatingWindow {
} }
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
} }
Connections { Connections {
@@ -83,6 +133,11 @@ FloatingWindow {
function onIsResponseRequiredChanged() { function onIsResponseRequiredChanged() {
if (!currentFlow.isResponseRequired) if (!currentFlow.isResponseRequired)
return; return;
if (awaitingFprintForPassword && passwordInput !== "") {
_commitSubmit();
return;
}
awaitingFprintForPassword = false;
isLoading = false; isLoading = false;
passwordInput = ""; passwordInput = "";
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
@@ -101,6 +156,41 @@ FloatingWindow {
} }
} }
FileView {
path: "/etc/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitEtcPamText = text()
onLoadFailed: root.polkitEtcPamText = ""
}
FileView {
path: "/usr/lib/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitLibPamText = text()
onLoadFailed: root.polkitLibPamText = ""
}
FileView {
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: root.systemAuthPamText = text()
onLoadFailed: root.systemAuthPamText = ""
}
FileView {
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: root.commonAuthPamText = text()
onLoadFailed: root.commonAuthPamText = ""
}
FileView {
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: root.passwordAuthPamText = text()
onLoadFailed: root.passwordAuthPamText = ""
}
FocusScope { FocusScope {
id: contentFocusScope id: contentFocusScope
@@ -205,36 +295,30 @@ FloatingWindow {
visible: text !== "" visible: text !== ""
} }
Rectangle { DankTextField {
id: passwordField
width: parent.width width: parent.width
height: inputFieldHeight height: inputFieldHeight
radius: Theme.cornerRadius backgroundColor: Theme.surfaceHover
color: Theme.surfaceHover normalBorderColor: Theme.outlineStrong
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong focusedBorderColor: Theme.primary
border.width: passwordField.activeFocus ? 2 : 1 borderWidth: 1
focusedBorderWidth: 2
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
leftIconSize: 20
leftIconColor: Theme.primary
leftIconFocusedColor: Theme.primary
opacity: isLoading ? 0.5 : 1 opacity: isLoading ? 0.5 : 1
font.pixelSize: Theme.fontSizeMedium
MouseArea { textColor: Theme.surfaceText
anchors.fill: parent text: passwordInput
enabled: !isLoading showPasswordToggle: !(currentFlow?.responseVisible ?? false)
onClicked: passwordField.forceActiveFocus() echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
} placeholderText: ""
enabled: !isLoading
DankTextField { onTextEdited: passwordInput = text
id: passwordField onAccepted: submitAuth()
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: passwordInput = text
onAccepted: submitAuth()
}
} }
StyledText { StyledText {

View File

@@ -241,6 +241,21 @@ FocusScope {
} }
} }
Loader {
id: greeterLoader
anchors.fill: parent
active: root.currentIndex === 31
visible: active
focus: active
sourceComponent: GreeterTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader { Loader {
id: pluginsLoader id: pluginsLoader
anchors.fill: parent anchors.fill: parent
@@ -470,7 +485,7 @@ FocusScope {
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
@@ -485,7 +500,7 @@ FocusScope {
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
} }

View File

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

View File

@@ -0,0 +1,20 @@
.pragma library
function readBoolOverride(envReader, names, fallbackValue) {
for (let i = 0; i < names.length; i++) {
const name = names[i];
const raw = envReader(name);
if (raw === undefined || raw === null || raw === "")
continue;
const normalized = String(raw).trim().toLowerCase();
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
return true;
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
return false;
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
}
return fallbackValue;
}

View File

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

View File

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

View File

@@ -31,6 +31,25 @@ Item {
signal launchRequested signal launchRequested
property bool weatherInitialized: false property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property bool pendingPasswordResponse: false
property bool passwordSubmitRequested: false
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 12000
property int externalAuthTimeoutMs: 45000
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string faillockConfigText: ""
property bool greeterWallpaperOverrideExists: false
property string externalAuthAutoStartedForUser: ""
readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"))
readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f"))
readonly property bool greeterExternalAuthAvailable: greeterPamHasFprint || greeterPamHasU2f
function initWeatherService() { function initWeatherService() {
if (weatherInitialized) if (weatherInitialized)
@@ -44,16 +63,253 @@ Item {
WeatherService.forceRefresh(); WeatherService.forceRefresh();
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function usesPamLockoutPolicy(pamText) {
if (!pamText)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
return true;
}
return false;
}
function parsePamLineDenyValue(pamText) {
if (!pamText)
return -1;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
continue;
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function parseFaillockDenyValue(configText) {
if (!configText)
return -1;
const lines = configText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function refreshPasswordAttemptPolicyHint() {
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText];
let lockoutConfigured = false;
let denyFromPam = -1;
for (let i = 0; i < pamSources.length; i++) {
const source = pamSources[i];
if (!source)
continue;
if (usesPamLockoutPolicy(source))
lockoutConfigured = true;
const denyValue = parsePamLineDenyValue(source);
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
denyFromPam = denyValue;
}
if (!lockoutConfigured) {
passwordAttemptLimitHint = 0;
return;
}
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
if (denyFromConfig >= 0) {
passwordAttemptLimitHint = denyFromConfig;
return;
}
if (denyFromPam >= 0) {
passwordAttemptLimitHint = denyFromPam;
return;
}
// pam_faillock default deny value when no explicit config is set.
passwordAttemptLimitHint = 3;
}
function isLikelyLockoutMessage(message) {
const lower = (message || "").toLowerCase();
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
}
function currentAuthMessage() {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "max")
return "Too many failed attempts - account may be locked";
if (GreeterState.pamState === "fail") {
if (passwordAttemptLimitHint > 0) {
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
if (remaining > 0) {
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
}
return "Incorrect password - next failures may trigger account lockout";
}
return "Incorrect password";
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
}
Connections { Connections {
target: GreetdSettings target: GreetdSettings
function onSettingsLoadedChanged() { function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) if (GreetdSettings.settingsLoaded) {
initWeatherService(); initWeatherService();
if (isPrimaryScreen) {
applyLastSuccessfulUser();
finalizeSessionSelection();
}
}
}
function onRememberLastUserChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
applyLastSuccessfulUser();
}
function onRememberLastSessionChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
finalizeSessionSelection();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: faillockConfigWatcher
path: "/etc/security/faillock.conf"
printErrors: false
onLoaded: {
root.faillockConfigText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.faillockConfigText = "";
root.refreshPasswordAttemptPolicyHint();
} }
} }
Component.onCompleted: { Component.onCompleted: {
initWeatherService(); initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen) if (isPrimaryScreen)
applyLastSuccessfulUser(); applyLastSuccessfulUser();
@@ -63,15 +319,116 @@ Item {
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser; const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser; GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser; GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true; GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser); PortalService.getGreeterUserProfileImage(lastUser);
maybeAutoStartExternalAuth();
} }
} }
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
maybeAutoStartExternalAuth();
}
function submitBufferedPassword() {
if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0)
return false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "";
inputField.text = "";
return true;
}
function requestPasswordSessionTransition() {
if (cancelingExternalAuthForPassword)
return;
cancelingExternalAuthForPassword = true;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
Greetd.cancelSession();
}
function startAuthSession() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking)
return;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (Greetd.state !== GreetdState.Inactive) {
if (pendingPasswordResponse && hasPasswordBuffer)
submitBufferedPassword();
else if (awaitingExternalAuth && hasPasswordBuffer) {
passwordSubmitRequested = true;
} else if (hasPasswordBuffer)
passwordSubmitRequested = true;
return;
}
if (cancelingExternalAuthForPassword) {
if (hasPasswordBuffer)
passwordSubmitRequested = true;
return;
}
if (!hasPasswordBuffer && !root.greeterExternalAuthAvailable)
return;
pendingPasswordResponse = false;
passwordSubmitRequested = hasPasswordBuffer;
awaitingExternalAuth = !hasPasswordBuffer && root.greeterExternalAuthAvailable;
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function maybeAutoStartExternalAuth() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (!root.greeterExternalAuthAvailable)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
return;
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
return;
if (externalAuthAutoStartedForUser === GreeterState.username)
return;
externalAuthAutoStartedForUser = GreeterState.username;
startAuthSession();
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized) if (weatherInitialized)
WeatherService.removeRef(); WeatherService.removeRef();
@@ -143,10 +500,39 @@ Item {
} }
} }
FileView {
id: greeterWallpaperOverrideFile
path: GreetdSettings.greeterWallpaperOverridePath
printErrors: false
watchChanges: true
onLoaded: root.greeterWallpaperOverrideExists = true
onLoadFailed: root.greeterWallpaperOverrideExists = false
}
Connections {
target: GreetdSettings
function onGreeterWallpaperOverridePathChanged() {
if (!GreetdSettings.greeterWallpaperOverridePath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
function onGreeterWallpaperPathChanged() {
if (!GreetdSettings.greeterWallpaperPath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
}
DankBackdrop { DankBackdrop {
anchors.fill: parent anchors.fill: parent
screenName: root.screenName screenName: root.screenName
visible: { visible: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return false;
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
@@ -159,12 +545,14 @@ Item {
anchors.fill: parent anchors.fill: parent
source: { source: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : ""; return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
} }
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode) fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
smooth: true smooth: true
asynchronous: false asynchronous: false
cache: true cache: true
@@ -327,10 +715,7 @@ Item {
anchors.top: clockContainer.bottom anchors.top: clockContainer.bottom
anchors.topMargin: 4 anchors.topMargin: 4
text: { text: {
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) { return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
}
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
} }
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: "white" color: "white"
@@ -399,6 +784,9 @@ Item {
if (GreeterState.showPasswordInput && revealButton.visible) { if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width; margin += revealButton.width;
} }
if (externalAuthButton.visible) {
margin += externalAuthButton.width;
}
if (virtualKeyboardButton.visible) { if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width; margin += virtualKeyboardButton.width;
} }
@@ -415,21 +803,18 @@ Item {
return; return;
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text; GreeterState.passwordBuffer = text;
if (!text || text.length === 0)
root.passwordSubmitRequested = false;
} else { } else {
GreeterState.usernameInput = text; GreeterState.usernameInput = text;
} }
} }
onAccepted: { onAccepted: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) { root.startAuthSession();
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (text.trim()) { if (text.trim()) {
GreeterState.username = text.trim(); root.submitUsername(text);
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true; syncingFromState = true;
text = ""; text = "";
syncingFromState = false; syncingFromState = false;
@@ -461,14 +846,14 @@ Item {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (GreeterState.unlocking) { if (GreeterState.unlocking) {
return "Logging in..."; return "Logging in...";
} }
if (Greetd.state !== GreetdState.Inactive) { if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
return "Authenticating..."; return "Authenticating...";
} }
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
@@ -476,7 +861,7 @@ Item {
} }
return "Username..."; return "Username...";
} }
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline) color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0 opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
@@ -498,7 +883,7 @@ Item {
StyledText { StyledText {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -528,15 +913,27 @@ Item {
DankActionButton { DankActionButton {
id: revealButton id: revealButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right) anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
anchors.rightMargin: 0 anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility" iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32 buttonSize: 32
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: parent.showPassword = !parent.showPassword onClicked: parent.showPassword = !parent.showPassword
} }
DankActionButton {
id: externalAuthButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
buttonSize: 32
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible
onClicked: root.startAuthSession()
}
DankActionButton { DankActionButton {
id: virtualKeyboardButton id: virtualKeyboardButton
@@ -545,7 +942,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboard_controller.isKeyboardActive) { if (keyboard_controller.isKeyboardActive) {
@@ -564,19 +961,14 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return" iconName: "keyboard_return"
buttonSize: 36 buttonSize: 36
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: true enabled: true
onClicked: { onClicked: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (GreeterState.username) { root.startAuthSession();
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (inputField.text.trim()) { if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim(); root.submitUsername(inputField.text);
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
} }
} }
@@ -601,20 +993,16 @@ Item {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 20 Layout.preferredHeight: 38
Layout.topMargin: -Theme.spacingS Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS Layout.bottomMargin: -Theme.spacingS
text: { text: root.authFeedbackMessage
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password";
return "";
}
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
opacity: GreeterState.pamState !== "" ? 1 : 0 wrapMode: Text.WordWrap
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -667,6 +1055,7 @@ Item {
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: { onClicked: {
GreeterState.reset(); GreeterState.reset();
root.externalAuthAutoStartedForUser = "";
inputField.text = ""; inputField.text = "";
PortalService.profileImage = ""; PortalService.profileImage = "";
} }
@@ -1029,9 +1418,11 @@ Item {
return; return;
if (!GreetdMemory.memoryReady) if (!GreetdMemory.memoryReady)
return; return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdMemory.lastSessionId; const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
if (savedSession) { if (savedSession && GreetdSettings.rememberLastSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) { if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i; GreeterState.currentSessionIndex = i;
@@ -1164,44 +1555,125 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) { function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) { if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer); cancelingExternalAuthForPassword = false;
GreeterState.passwordBuffer = ""; awaitingExternalAuth = false;
inputField.text = ""; authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
pendingPasswordResponse = true;
if (passwordSubmitRequested && !root.submitBufferedPassword())
passwordSubmitRequested = false;
return; return;
} }
if (!error) pendingPasswordResponse = false;
Greetd.respond(""); if (!passwordSubmitRequested)
awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired);
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond("");
}
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested && GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
if (resumePasswordSubmit) {
Qt.callLater(root.startAuthSession);
return;
}
passwordSubmitRequested = false;
}
} }
function onReadyToLaunch() { function onReadyToLaunch() {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
passwordFailureCount = 0;
clearAuthFeedback();
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]; const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex]; const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
if (!sessionCmd) { if (!sessionCmd) {
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
return; return;
} }
GreeterState.unlocking = true; GreeterState.unlocking = true;
launchTimeout.restart(); launchTimeout.restart();
GreetdMemory.setLastSessionId(sessionPath); if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username); GreetdMemory.setLastSessionId(sessionPath);
} else if (GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]); Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
} }
function onAuthFailure(message) { function onAuthFailure(message) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "fail"; if (isLikelyLockoutMessage(message)) {
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession();
} }
function onError(error) { function onError(error) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
@@ -1215,8 +1687,12 @@ Item {
onTriggered: { onTriggered: {
if (!GreeterState.unlocking) if (!GreeterState.unlocking)
return; return;
pendingPasswordResponse = false;
passwordSubmitRequested = false;
cancelingExternalAuthForPassword = false;
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession(); Greetd.cancelSession();
} }
@@ -1225,7 +1701,7 @@ Item {
Timer { Timer {
id: placeholderDelay id: placeholderDelay
interval: 4000 interval: 4000
onTriggered: GreeterState.pamState = "" onTriggered: clearAuthFeedback()
} }
LockPowerMenu { LockPowerMenu {

View File

@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc. - **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd` - **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
- **Session Memory**: Remembers last selected session and user - **Session Memory**: Remembers last selected session and user
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
## Installation ## Installation
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
dms-greeter --command sway dms-greeter --command sway
dms-greeter --command mangowc dms-greeter --command mangowc
dms-greeter --command niri -C /path/to/custom-niri.kdl dms-greeter --command niri -C /path/to/custom-niri.kdl
dms-greeter --command niri --remember-last-user false --remember-last-session false
``` ```
Configure greetd to use it in `/etc/greetd/config.toml`: Configure greetd to use it in `/etc/greetd/config.toml`:

View File

@@ -6,6 +6,9 @@ COMPOSITOR=""
COMPOSITOR_CONFIG="" COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter" DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter" CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() { show_help() {
cat << EOF cat << EOF
@@ -22,6 +25,15 @@ Options:
(default: dms-greeter) (default: dms-greeter)
--cache-dir PATH Cache directory for greeter data --cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter) (default: /var/cache/dms-greeter)
--remember-last-session BOOL
Persist selected session to greeter memory
(BOOL: true/false, default: from settings.json)
--remember-last-user BOOL
Persist last successful username to greeter memory
(BOOL: true/false, default: from settings.json)
--no-save-session Alias for --remember-last-session false
--no-save-username Alias for --remember-last-user false
--debug Enable verbose startup logging to stderr
-h, --help Show this help message -h, --help Show this help message
Examples: Examples:
@@ -30,6 +42,7 @@ Examples:
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango dms-greeter --command mango
dms-greeter --command labwc dms-greeter --command labwc
EOF EOF
@@ -43,6 +56,41 @@ require_command() {
fi fi
} }
normalize_bool_flag() {
local flag_name="$1"
local value="$2"
local normalized="${value,,}"
case "$normalized" in
1|true|yes|on)
echo "1"
;;
0|false|no|off)
echo "0"
;;
*)
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
exit 1
;;
esac
}
exec_compositor() {
local log_tag="$1"
shift
if [[ "$DEBUG_MODE" == "1" ]]; then
exec "$@"
fi
if command -v systemd-cat >/dev/null 2>&1; then
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
fi
local log_file="$CACHE_DIR/$log_tag.log"
exec "$@" >> "$log_file" 2>&1
}
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--command) --command)
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
CACHE_DIR="$2" CACHE_DIR="$2"
shift 2 shift 2
;; ;;
--remember-last-session)
REMEMBER_LAST_SESSION="$2"
shift 2
;;
--remember-last-user)
REMEMBER_LAST_USER="$2"
shift 2
;;
--no-save-session)
REMEMBER_LAST_SESSION="0"
shift
;;
--no-save-username)
REMEMBER_LAST_USER="0"
shift
;;
--debug)
DEBUG_MODE=1
shift
;;
-h|--help) -h|--help)
show_help show_help
exit 0 exit 0
@@ -113,7 +181,45 @@ export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1 export DMS_RUN_GREETER=1
export DMS_GREET_CFG_DIR="$CACHE_DIR" export DMS_GREET_CFG_DIR="$CACHE_DIR"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
mkdir -p "$CACHE_DIR" mkdir -p "$CACHE_DIR"
mkdir -p "$CACHE_DIR/.local/state"
mkdir -p "$CACHE_DIR/.local/share"
mkdir -p "$CACHE_DIR/.cache"
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
if command -v qs >/dev/null 2>&1; then if command -v qs >/dev/null 2>&1; then
QS_BIN="qs" QS_BIN="qs"
@@ -130,7 +236,9 @@ if [[ "$DMS_PATH" == /* ]]; then
else else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH") RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2 if [[ "$DEBUG_MODE" == "1" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
QS_CMD="$QS_BIN -p $RESOLVED_PATH" QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2 echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
@@ -192,7 +300,7 @@ NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation" spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec niri -c "$COMPOSITOR_CONFIG" exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
;; ;;
hyprland) hyprland)
@@ -222,9 +330,9 @@ HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
if command -v start-hyprland >/dev/null 2>&1; then if command -v start-hyprland >/dev/null 2>&1; then
exec start-hyprland -- --config "$COMPOSITOR_CONFIG" exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
else else
exec Hyprland -c "$COMPOSITOR_CONFIG" exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
fi fi
;; ;;
@@ -245,7 +353,7 @@ exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG" exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;; ;;
scroll) scroll)
@@ -265,7 +373,7 @@ exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec scroll -c "$COMPOSITOR_CONFIG" exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
;; ;;
miracle|miracle-wm) miracle|miracle-wm)
@@ -285,24 +393,24 @@ exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec miracle-wm -c "$COMPOSITOR_CONFIG" exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
;; ;;
labwc) labwc)
require_command "labwc" require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD" exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else else
exec labwc --session "$QS_CMD" exec_compositor "labwc" labwc --session "$QS_CMD"
fi fi
;; ;;
mango|mangowc) mango|mangowc)
require_command "mango" require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
else else
exec mango -s "$QS_CMD && mmsg -d quit" exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
fi fi
;; ;;

View File

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

View File

@@ -25,6 +25,29 @@ Scope {
signal flashMsg signal flashMsg
signal unlockRequested signal unlockRequested
function resetAuthFlows(): void {
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockInProgress = false;
}
function recoverFromAuthStall(newState: string): void {
resetAuthFlows();
state = newState;
flashMsg();
stateReset.restart();
fprint.checkAvail();
u2f.checkAvail();
}
function completeUnlock(): void { function completeUnlock(): void {
if (!unlockInProgress) { if (!unlockInProgress) {
unlockInProgress = true; unlockInProgress = true;
@@ -36,6 +59,7 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; u2fPending = false;
u2fState = ""; u2fState = "";
unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
} }
} }
@@ -102,6 +126,13 @@ Scope {
return; return;
} }
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error) if (res === PamResult.Error)
root.state = "error"; root.state = "error";
else if (res === PamResult.MaxTries) else if (res === PamResult.MaxTries)
@@ -114,6 +145,18 @@ Scope {
} }
} }
Connections {
target: passwd
function onActiveChanged() {
if (passwd.active) {
passwdActiveTimeout.restart();
} else {
passwdActiveTimeout.running = false;
}
}
}
PamContext { PamContext {
id: fprint id: fprint
@@ -241,7 +284,7 @@ Scope {
Process { Process {
id: availProc id: availProc
command: ["sh", "-c", "fprintd-list $USER"] command: ["sh", "-c", "fprintd-list \"${USER:-$(id -un)}\""]
onExited: code => { onExited: code => {
fprint.available = code === 0; fprint.available = code === 0;
fprint.checkAvail(); fprint.checkAvail();
@@ -279,6 +322,26 @@ Scope {
onTriggered: root.cancelU2fPending() onTriggered: root.cancelU2fPending()
} }
Timer {
id: passwdActiveTimeout
interval: 15000
onTriggered: {
if (passwd.active)
root.recoverFromAuthStall("error");
}
}
Timer {
id: unlockRequestTimeout
interval: 8000
onTriggered: {
if (root.unlockInProgress)
root.recoverFromAuthStall("error");
}
}
Timer { Timer {
id: stateReset id: stateReset
@@ -308,17 +371,9 @@ Scope {
root.u2fState = ""; root.u2fState = "";
root.u2fPending = false; root.u2fPending = false;
root.lockMessage = ""; root.lockMessage = "";
root.unlockInProgress = false; root.resetAuthFlows();
} else { } else {
fprint.abort(); root.resetAuthFlows();
passwd.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
root.unlockInProgress = false;
} }
} }
@@ -338,6 +393,7 @@ Scope {
u2f.abort(); u2f.abort();
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fState = ""; root.u2fState = "";
u2f.checkAvail(); u2f.checkAvail();

View File

@@ -0,0 +1,587 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
FileBrowserModal {
id: greeterWallpaperBrowserModal
browserTitle: I18n.tr("Select greeter background image")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"]
onFileSelected: path => {
SettingsData.set("greeterWallpaperPath", path);
close();
}
}
property string greeterStatusText: ""
property bool greeterStatusRunning: false
property bool greeterSyncRunning: false
property string greeterStatusStdout: ""
property string greeterStatusStderr: ""
property string greeterSyncStdout: ""
property string greeterSyncStderr: ""
property string greeterSudoProbeStderr: ""
property string greeterTerminalFallbackStderr: ""
property bool greeterTerminalFallbackFromPrecheck: false
property var cachedFontFamilies: []
property bool fontsEnumerated: false
function runGreeterStatus() {
greeterStatusText = "";
greeterStatusStdout = "";
greeterStatusStderr = "";
greeterStatusRunning = true;
greeterStatusProcess.running = true;
}
function runGreeterSync() {
greeterSyncStdout = "";
greeterSyncStderr = "";
greeterSudoProbeStderr = "";
greeterTerminalFallbackStderr = "";
greeterTerminalFallbackFromPrecheck = false;
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…");
greeterSyncRunning = true;
greeterSudoProbeProcess.running = true;
}
function launchGreeterSyncTerminalFallback(fromPrecheck, statusText) {
greeterTerminalFallbackFromPrecheck = fromPrecheck;
if (statusText && statusText !== "")
greeterStatusText = statusText;
greeterTerminalFallbackStderr = "";
greeterTerminalFallbackProcess.running = true;
}
function enumerateFonts() {
if (fontsEnumerated)
return;
var fonts = [];
var availableFonts = Qt.fontFamilies();
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i];
if (fontName.startsWith("."))
continue;
fonts.push(fontName);
}
fonts.sort();
fonts.unshift("Default");
cachedFontFamilies = fonts;
fontsEnumerated = true;
}
Component.onCompleted: Qt.callLater(enumerateFonts)
Process {
id: greeterStatusProcess
command: ["dms", "greeter", "status"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.greeterStatusStdout = text || "";
}
}
stderr: StdioCollector {
onStreamFinished: root.greeterStatusStderr = text || ""
}
onExited: exitCode => {
root.greeterStatusRunning = false;
const out = (root.greeterStatusStdout || "").trim();
const err = (root.greeterStatusStderr || "").trim();
if (exitCode === 0) {
root.greeterStatusText = out !== "" ? out : I18n.tr("No status output.");
if (err !== "")
root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err;
return;
}
var failure = I18n.tr("Failed to run 'dms greeter status'. Ensure DMS is installed and dms is in PATH.", "greeter status error") + " (exit " + exitCode + ")";
if (out !== "")
failure = failure + "\n\n" + out;
if (err !== "")
failure = failure + "\n\nstderr:\n" + err;
root.greeterStatusText = failure;
}
}
Process {
id: greeterSyncProcess
command: ["dms", "greeter", "sync", "--yes"]
running: false
stdout: StdioCollector {
onStreamFinished: root.greeterSyncStdout = text || ""
}
stderr: StdioCollector {
onStreamFinished: root.greeterSyncStderr = text || ""
}
onExited: exitCode => {
root.greeterSyncRunning = false;
const out = (root.greeterSyncStdout || "").trim();
const err = (root.greeterSyncStderr || "").trim();
if (exitCode === 0) {
var success = I18n.tr("Sync completed successfully.");
if (out !== "")
success = success + "\n\n" + out;
if (err !== "")
success = success + "\n\nstderr:\n" + err;
root.greeterStatusText = success;
} else {
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
if (out !== "")
failure = failure + "\n\n" + out;
if (err !== "")
failure = failure + "\n\nstderr:\n" + err;
root.greeterStatusText = failure;
root.launchGreeterSyncTerminalFallback(false, "");
}
}
}
Process {
id: greeterSudoProbeProcess
command: ["sudo", "-n", "true"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterSudoProbeStderr = text || ""
}
onExited: exitCode => {
const err = (root.greeterSudoProbeStderr || "").trim();
if (exitCode === 0) {
root.greeterStatusText = I18n.tr("Running greeter sync…");
greeterSyncProcess.running = true;
return;
}
var authNeeded = I18n.tr("Sync needs sudo authentication. Opening terminal so you can use password or fingerprint.");
if (err !== "")
authNeeded = authNeeded + "\n\n" + err;
root.launchGreeterSyncTerminalFallback(true, authNeeded);
}
}
Process {
id: greeterTerminalFallbackProcess
command: ["dms", "greeter", "sync", "--terminal", "--yes"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterTerminalFallbackStderr = text || ""
}
onExited: exitCode => {
root.greeterSyncRunning = false;
if (exitCode === 0) {
var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done.");
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched;
return;
}
var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")";
const err = (root.greeterTerminalFallbackStderr || "").trim();
if (err !== "")
fallback = fallback + "\n\nstderr:\n" + err;
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + fallback : fallback;
}
}
readonly property var _lockDateFormatPresets: [
{
format: "",
label: I18n.tr("System Default", "date format option")
},
{
format: "ddd d",
label: I18n.tr("Day Date", "date format option")
},
{
format: "ddd MMM d",
label: I18n.tr("Day Month Date", "date format option")
},
{
format: "MMM d",
label: I18n.tr("Month Date", "date format option")
},
{
format: "M/d",
label: I18n.tr("Numeric (M/D)", "date format option")
},
{
format: "d/M",
label: I18n.tr("Numeric (D/M)", "date format option")
},
{
format: "ddd d MMM yyyy",
label: I18n.tr("Full with Year", "date format option")
},
{
format: "yyyy-MM-dd",
label: I18n.tr("ISO Date", "date format option")
},
{
format: "dddd, MMMM d",
label: I18n.tr("Full Day & Month", "date format option")
}
]
readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
width: parent.width
iconName: "info"
title: I18n.tr("Greeter Status")
settingKey: "greeterStatus"
StyledText {
text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
Item {
width: 1
height: Theme.spacingS
}
Rectangle {
width: parent.width
height: Math.min(180, statusTextArea.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
StyledText {
id: statusTextArea
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.greeterStatusRunning ? I18n.tr("Checking…", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
Row {
width: parent.width
spacing: Theme.spacingS
topPadding: Theme.spacingM
DankButton {
text: I18n.tr("Refresh")
iconName: "refresh"
onClicked: root.runGreeterStatus()
enabled: !root.greeterStatusRunning
}
DankButton {
text: I18n.tr("Sync")
iconName: "sync"
onClicked: root.runGreeterSync()
enabled: !root.greeterSyncRunning
}
}
}
SettingsCard {
width: parent.width
iconName: "fingerprint"
title: I18n.tr("Login Authentication")
settingKey: "greeterAuth"
StyledText {
text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
SettingsToggleRow {
settingKey: "greeterEnableFprint"
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
text: I18n.tr("Enable fingerprint at login")
description: {
if (!SettingsData.fprintdAvailable)
return I18n.tr("Not available — install fprintd and enroll fingerprints.");
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.");
}
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableFprint
enabled: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
}
SettingsToggleRow {
settingKey: "greeterEnableU2f"
tags: ["greeter", "u2f", "security", "key", "login", "auth"]
text: I18n.tr("Enable security key at login")
description: {
if (!SettingsData.u2fAvailable)
return I18n.tr("Not available — install pam_u2f and enroll keys.");
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Disabled.");
}
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableU2f
enabled: SettingsData.u2fAvailable
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
}
}
SettingsCard {
width: parent.width
iconName: "palette"
title: I18n.tr("Greeter Appearance")
settingKey: "greeterAppearance"
StyledText {
text: I18n.tr("Font")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
SettingsDropdownRow {
settingKey: "greeterFontFamily"
tags: ["greeter", "font", "typography"]
text: I18n.tr("Greeter font")
description: I18n.tr("Font used on the login screen")
options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"]
currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default")
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
onValueChanged: value => {
if (value === "Default")
SettingsData.set("greeterFontFamily", "");
else
SettingsData.set("greeterFontFamily", value);
}
}
StyledText {
text: I18n.tr("Time format")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
SettingsToggleRow {
settingKey: "greeterUse24Hour"
tags: ["greeter", "time", "24hour"]
text: I18n.tr("24-hour clock")
description: I18n.tr("Greeter only — does not affect main clock")
checked: SettingsData.greeterUse24HourClock
onToggled: checked => SettingsData.set("greeterUse24HourClock", checked)
}
SettingsToggleRow {
settingKey: "greeterShowSeconds"
tags: ["greeter", "time", "seconds"]
text: I18n.tr("Show seconds")
checked: SettingsData.greeterShowSeconds
onToggled: checked => SettingsData.set("greeterShowSeconds", checked)
}
SettingsToggleRow {
settingKey: "greeterPadHours"
tags: ["greeter", "time", "12hour"]
text: I18n.tr("Pad hours (02:00 vs 2:00)")
visible: !SettingsData.greeterUse24HourClock
checked: SettingsData.greeterPadHours12Hour
onToggled: checked => SettingsData.set("greeterPadHours12Hour", checked)
}
StyledText {
text: I18n.tr("Date format on greeter")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
SettingsDropdownRow {
settingKey: "greeterLockDateFormat"
tags: ["greeter", "date", "format"]
text: I18n.tr("Date format")
description: I18n.tr("Greeter only — format for the date on the login screen")
options: root._lockDateFormatPresets.map(p => p.label)
currentValue: {
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || "";
var match = root._lockDateFormatPresets.find(p => p.format === current);
return match ? match.label : (current ? I18n.tr("Custom: ") + current : root._lockDateFormatPresets[0].label);
}
onValueChanged: value => {
var preset = root._lockDateFormatPresets.find(p => p.label === value);
SettingsData.set("greeterLockDateFormat", preset ? preset.format : "");
}
}
StyledText {
text: I18n.tr("Background")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
topPadding: Theme.spacingM
}
StyledText {
text: I18n.tr("Use a custom image for the login screen, or leave empty to use your desktop wallpaper.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
Row {
width: parent.width
spacing: Theme.spacingS
DankTextField {
id: greeterWallpaperPathField
width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS
placeholderText: I18n.tr("Use desktop wallpaper")
text: SettingsData.greeterWallpaperPath
backgroundColor: Theme.surfaceContainerHighest
onTextChanged: {
if (text !== SettingsData.greeterWallpaperPath)
SettingsData.set("greeterWallpaperPath", text);
}
}
DankButton {
id: browseGreeterWallpaperButton
text: I18n.tr("Browse")
onClicked: greeterWallpaperBrowserModal.open()
}
}
SettingsDropdownRow {
settingKey: "greeterWallpaperFillMode"
tags: ["greeter", "wallpaper", "background", "fill"]
text: I18n.tr("Wallpaper fill mode")
description: I18n.tr("How the background image is scaled")
options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode"))
currentValue: {
var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill");
var idx = root._wallpaperFillModes.indexOf(mode);
return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode");
}
onValueChanged: value => {
var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value);
if (idx >= 0)
SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]);
}
}
StyledText {
text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
}
SettingsCard {
width: parent.width
iconName: "history"
title: I18n.tr("Greeter Behavior")
settingKey: "greeterBehavior"
StyledText {
text: I18n.tr("Convenience options for the login screen. Sync to apply.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
SettingsToggleRow {
settingKey: "greeterRememberLastSession"
tags: ["greeter", "session", "remember", "login"]
text: I18n.tr("Remember last session")
description: I18n.tr("Pre-select the last used session on the greeter")
checked: SettingsData.greeterRememberLastSession
onToggled: checked => SettingsData.set("greeterRememberLastSession", checked)
}
SettingsToggleRow {
settingKey: "greeterRememberLastUser"
tags: ["greeter", "user", "remember", "login", "username"]
text: I18n.tr("Remember last user")
description: I18n.tr("Pre-fill the last successful username on the greeter")
checked: SettingsData.greeterRememberLastUser
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
}
}
SettingsCard {
width: parent.width
iconName: "extension"
title: I18n.tr("Dependencies & documentation")
settingKey: "greeterDeps"
StyledText {
text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
StyledText {
text: I18n.tr("Installation and PAM setup: see the ") + "<a href=\"https://danklinux.com/docs/dankgreeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">DankGreeter docs</a> " + I18n.tr("or run ") + "'dms greeter install'."
textFormat: Text.RichText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
linkColor: Theme.primary
width: parent.width
wrapMode: Text.Wrap
onLinkActivated: url => Qt.openUrlExternally(url)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
}
}
}

View File

@@ -245,7 +245,7 @@ Item {
StyledText { StyledText {
text: I18n.tr("Path to a video file or folder containing videos") text: I18n.tr("Path to a video file or folder containing videos")
font.pixelSize: Theme.fontSizeXSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineVariant color: Theme.outlineVariant
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width

View File

@@ -1,3 +1,3 @@
#%PAM-1.0 #%PAM-1.0
auth required pam_fprintd.so max-tries=1 auth required pam_fprintd.so max-tries=1 timeout=5