1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00

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
This commit is contained in:
purian23
2026-03-05 23:04:59 -05:00
parent 32d16d0673
commit fe5bd42e25
18 changed files with 1814 additions and 132 deletions

View File

@@ -39,12 +39,29 @@ var greeterSyncCmd = &cobra.Command{
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",
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)
}
},
}
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{
Use: "enable",
Short: "Enable DMS greeter in greetd config",
@@ -147,7 +164,7 @@ func installGreeter() error {
}
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
}
@@ -171,22 +188,88 @@ func installGreeter() error {
return nil
}
func syncGreeter() error {
fmt.Println("=== DMS Greeter Theme Sync ===")
fmt.Println()
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
syncFlags := make([]string, 0, 3)
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) {
fmt.Println(msg)
}
fmt.Println("Detecting DMS installation...")
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
return err
if !nonInteractive {
fmt.Println("Detecting DMS installation...")
}
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 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.Print("Would you like to enable it now? (Y/n): ")
@@ -203,9 +286,12 @@ func syncGreeter() error {
}
}
cacheDir := "/var/cache/dms-greeter"
cacheDir := greeter.GreeterCacheDir
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()
@@ -224,6 +310,9 @@ func syncGreeter() error {
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
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("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
@@ -255,8 +344,14 @@ func syncGreeter() error {
return fmt.Errorf("no supported compositors found")
case 1:
compositor = compositors[0]
fmt.Printf("✓ Using compositor: %s\n", compositor)
if !nonInteractive {
fmt.Printf("✓ Using compositor: %s\n", compositor)
}
default:
if nonInteractive {
compositor = compositors[0]
break
}
var err error
compositor, err = promptCompositorChoice(compositors)
if err != nil {
@@ -264,27 +359,159 @@ func syncGreeter() error {
}
fmt.Printf("✓ Selected compositor: %s\n", compositor)
}
} else {
} else if !nonInteractive {
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...")
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
return err
}
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
}
fmt.Println("\n=== Sync Complete ===")
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.")
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) {
state, err := getSystemdServiceState(dmName)
if err != nil {
@@ -497,12 +724,20 @@ func enableGreeter() error {
configAlreadyCorrect := isGreeterEnabled()
configuredCompositor := detectConfiguredCompositor()
logFunc := func(msg string) {
fmt.Println(msg)
}
if configAlreadyCorrect {
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 {
return err
}
@@ -556,13 +791,14 @@ func enableGreeter() error {
}
greeterPathForConfig = dmsPath
}
logFunc := func(msg string) {
fmt.Println(msg)
}
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
return fmt.Errorf("failed to configure greetd: %w", err)
}
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 {
return err
}
@@ -653,6 +889,95 @@ func readDefaultSessionCommand(configPath string) string {
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 {
osInfo, err := distros.GetOSInfo()
if err != nil {
@@ -731,11 +1056,19 @@ func checkGreeterStatus() error {
}
configPath := "/etc/greetd/config.toml"
configuredCommand := ""
allGood := true
fmt.Println("Greeter Configuration:")
if _, err := os.ReadFile(configPath); err == nil {
command := readDefaultSessionCommand(configPath)
if command != "" && strings.Contains(command, "dms-greeter") {
configuredCommand = readDefaultSessionCommand(configPath)
if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") {
fmt.Println(" ✓ Greeter is enabled")
if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
fmt.Printf(" Wrapper: %s\n", wrapper)
}
if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
fmt.Printf(" DMS path override: %s\n", pathOverride)
}
compositor := detectConfiguredCompositor()
switch compositor {
@@ -751,10 +1084,12 @@ func checkGreeterStatus() error {
} else {
fmt.Println(" ✗ Greeter is NOT enabled")
fmt.Println(" Run 'dms greeter enable' to enable it")
allGood = false
}
} else {
fmt.Println(" ✗ Greeter config not found")
fmt.Printf(" %s\n", packageInstallHint())
allGood = false
}
fmt.Println("\nGroup Membership:")
@@ -773,8 +1108,12 @@ func checkGreeterStatus() error {
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.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() {
fmt.Printf(" ✓ %s exists\n", cacheDir)
} else {
@@ -806,7 +1145,6 @@ func checkGreeterStatus() error {
},
}
allGood := true
for _, link := range symlinks {
targetInfo, err := os.Lstat(link.target)
if err != nil {
@@ -845,11 +1183,80 @@ func checkGreeterStatus() error {
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()
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 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

View File

@@ -3,6 +3,7 @@ package greeter
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
@@ -17,10 +18,29 @@ import (
"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) {
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 {
data, err := os.ReadFile("/etc/group")
if err != nil {
@@ -201,17 +221,29 @@ func DetectGreeterUser() string {
}
func resolveGreeterWrapperPath() string {
if path, err := exec.LookPath("dms-greeter"); err == nil {
return path
if override := strings.TrimSpace(os.Getenv("DMS_GREETER_WRAPPER_CMD")); override != "" {
return override
}
for _, candidate := range []string{"/usr/local/bin/dms-greeter", "/usr/bin/dms-greeter"} {
if _, err := os.Stat(candidate); err == nil {
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
}
}
return "dms-greeter"
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 {
@@ -514,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 {
return fmt.Errorf("failed to create cache directory: %w", err)
}
@@ -526,11 +572,10 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
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)
}
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
}
@@ -730,13 +775,13 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
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()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
cacheDir := "/var/cache/dms-greeter"
cacheDir := GreeterCacheDir
symlinks := []struct {
source string
@@ -764,28 +809,33 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
sourceDir := filepath.Dir(link.source)
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
continue
return fmt.Errorf("failed to create source directory %s for %s: %w", sourceDir, link.desc, err)
}
}
if _, err := os.Stat(link.source); os.IsNotExist(err) {
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
continue
return fmt.Errorf("failed to create source file %s for %s: %w", link.source, link.desc, err)
}
}
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
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))
continue
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
}
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" {
return nil
}
@@ -797,6 +847,293 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
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 {
processed map[string]bool
nodes []*document.Node
@@ -938,6 +1275,8 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
}
// Strip existing -C or --config and their arguments
command = stripConfigFlag(command)
command = stripCacheDirFlag(command)
command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir)
newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath)
idx := strings.Index(line, "command")
@@ -954,10 +1293,6 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
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")
if err != nil {
return fmt.Errorf("failed to create temp greetd config: %w", err)
@@ -988,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"))
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 {
@@ -1134,14 +1472,12 @@ func (s *niriGreeterSync) render() string {
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
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 {
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))
} else if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to access %s: %w", configPath, err)
}
greeterUser := DetectGreeterUser()
@@ -1162,7 +1498,7 @@ vt = 1
wrapperCmd := resolveGreeterWrapperPath()
compositorLower := strings.ToLower(compositor)
commandValue := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower)
commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
if dmsPath != "" {
commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath)
}
@@ -1227,6 +1563,30 @@ func stripConfigFlag(command string) string {
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.
func getDebianOBSSlug(osInfo *distros.OSInfo) string {
versionID := strings.ToLower(osInfo.VersionID)
@@ -1403,7 +1763,7 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
}
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))
}