1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/core/cmd/dms/commands_greeter.go
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

1264 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var greeterCmd = &cobra.Command{
Use: "greeter",
Short: "Manage DMS greeter",
Long: "Manage DMS greeter (greetd)",
}
var greeterInstallCmd = &cobra.Command{
Use: "install",
Short: "Install and configure DMS greeter",
Long: "Install greetd and configure it to use DMS as the greeter interface",
Run: func(cmd *cobra.Command, args []string) {
if err := installGreeter(); err != nil {
log.Fatalf("Error installing greeter: %v", err)
}
},
}
var greeterSyncCmd = &cobra.Command{
Use: "sync",
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) {
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",
Long: "Configure greetd to use DMS as the greeter",
Run: func(cmd *cobra.Command, args []string) {
if err := enableGreeter(); err != nil {
log.Fatalf("Error enabling greeter: %v", err)
}
},
}
var greeterStatusCmd = &cobra.Command{
Use: "status",
Short: "Check greeter sync status",
Long: "Check the status of greeter installation and configuration sync",
Run: func(cmd *cobra.Command, args []string) {
if err := checkGreeterStatus(); err != nil {
log.Fatalf("Error checking greeter status: %v", err)
}
},
}
func installGreeter() error {
fmt.Println("=== DMS Greeter Installation ===")
logFunc := func(msg string) {
fmt.Println(msg)
}
if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil {
return err
}
// Debian/openSUSE
greeter.TryInstallGreeterPackage(logFunc, "")
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint())
}
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")
}
// If already fully configured, prompt the user
if isGreeterEnabled() {
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
var response string
fmt.Scanln(&response)
response = strings.TrimSpace(strings.ToLower(response))
if response == "n" || response == "no" {
fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.")
return nil
}
fmt.Println()
}
fmt.Println("\nDetecting DMS installation...")
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
return err
}
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
fmt.Println("\nDetecting installed compositors...")
compositors := greeter.DetectCompositors()
if len(compositors) == 0 {
return fmt.Errorf("no supported compositors found (niri or Hyprland required)")
}
var selectedCompositor string
if len(compositors) == 1 {
selectedCompositor = compositors[0]
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
} else {
var err error
selectedCompositor, err = greeter.PromptCompositorChoice(compositors)
if err != nil {
return err
}
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
}
fmt.Println("\nSetting up dms-greeter group and permissions...")
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
return err
}
fmt.Println("\nCopying greeter files...")
if err := greeter.CopyGreeterFiles(dmsPath, selectedCompositor, logFunc, ""); err != nil {
return err
}
fmt.Println("\nConfiguring greetd...")
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
greeterPathForConfig := ""
if !greeter.IsGreeterPackaged() {
greeterPathForConfig = dmsPath
}
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
return err
}
fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil {
return err
}
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Installation Complete ===")
fmt.Println("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at next boot.")
return nil
}
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)
}
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)
}
}
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): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "n" && response != "no" {
if err := enableGreeter(); err != nil {
return err
}
} else {
return fmt.Errorf("greeter must be enabled before syncing")
}
}
cacheDir := greeter.GreeterCacheDir
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
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()
greeterGroupExists := utils.HasGroup(greeterGroup)
if greeterGroupExists {
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
groupsCmd := exec.Command("groups", currentUser.Username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return fmt.Errorf("failed to check groups: %w", err)
}
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)
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "n" && response != "no" {
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
addUserCmd.Stdout = os.Stdout
addUserCmd.Stderr = os.Stderr
if err := addUserCmd.Run(); err != nil {
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
}
fmt.Printf("✓ User added to %s group\n", greeterGroup)
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
} else {
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
}
}
}
compositor := detectConfiguredCompositor()
if compositor == "" {
compositors := greeter.DetectCompositors()
switch len(compositors) {
case 0:
return fmt.Errorf("no supported compositors found")
case 1:
compositor = compositors[0]
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 {
return err
}
fmt.Printf("✓ Selected compositor: %s\n", compositor)
}
} 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, "", 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 {
return false, fmt.Errorf("failed to check %s state: %w", dmName, err)
}
if !state.Exists {
return false, nil
}
fmt.Printf("\nChecking %s...\n", dmName)
fmt.Printf(" Current state: enabled=%s\n", state.EnabledState)
actionTaken := false
if state.NeedsDisable {
var disableCmd *exec.Cmd
var actionVerb string
if state.EnabledState == "static" {
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
actionVerb = "masked"
} else {
fmt.Printf(" Disabling %s...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
actionVerb = "disabled"
}
disableCmd.Stdout = os.Stdout
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
}
enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName)
if verifyErr != nil {
fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr)
} else if shouldDisable {
return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb)
} else {
fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState)
}
actionTaken = true
} else {
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Printf(" ✓ %s is already masked\n", dmName)
} else {
fmt.Printf(" ✓ %s is already disabled\n", dmName)
}
}
return actionTaken, nil
}
func ensureGreetdEnabled() error {
fmt.Println("\nChecking greetd service status...")
state, err := getSystemdServiceState("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
}
if !state.Exists {
return fmt.Errorf("greetd service not found. Please install greetd first")
}
fmt.Printf(" Current state: %s\n", state.EnabledState)
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Println(" Unmasking greetd...")
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
unmaskCmd.Stdout = os.Stdout
unmaskCmd.Stderr = os.Stderr
if err := unmaskCmd.Run(); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err)
}
fmt.Println(" ✓ Unmasked greetd")
}
if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" {
fmt.Println(" Reasserting greetd as active display manager...")
} else {
fmt.Println(" Enabling greetd service...")
}
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd")
if verifyErr != nil {
fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr)
} else {
switch enabledState {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
fmt.Printf(" ✓ greetd enabled (state: %s)\n", enabledState)
default:
return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState)
}
}
return nil
}
func ensureGraphicalTarget() error {
getDefaultCmd := exec.Command("systemctl", "get-default")
currentTarget, err := getDefaultCmd.Output()
if err != nil {
fmt.Println("⚠ Warning: Could not detect current default systemd target")
return nil
}
currentTargetStr := strings.TrimSpace(string(currentTarget))
if currentTargetStr != "graphical.target" {
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
setDefaultCmd.Stdout = os.Stdout
setDefaultCmd.Stderr = os.Stderr
if err := setDefaultCmd.Run(); err != nil {
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
fmt.Println(" Greeter may not start on boot. Run manually:")
fmt.Println(" sudo systemctl set-default graphical.target")
return nil
}
fmt.Println("✓ Set graphical.target as default")
} else {
fmt.Println("✓ Default target already set to graphical.target")
}
return nil
}
func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
disabledAny := false
var errors []string
for _, dm := range conflictingDMs {
actionTaken, err := disableDisplayManager(dm)
if err != nil {
errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err)
errors = append(errors, errMsg)
fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg)
continue
}
if actionTaken {
disabledAny = true
}
}
if len(errors) > 0 {
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║")
fmt.Println("╚════════════════════════════════════════════════════════════╝")
fmt.Println("\nSome display managers could not be disabled:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
fmt.Println("\nThis may prevent greetd from starting properly.")
fmt.Println("You may need to manually disable them before greetd will work.")
fmt.Println("\nManual commands to try:")
for _, dm := range conflictingDMs {
fmt.Printf(" sudo systemctl disable %s\n", dm)
fmt.Printf(" sudo systemctl mask %s\n", dm)
}
fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response == "n" || response == "no" {
return fmt.Errorf("aborted due to display manager conflicts")
}
fmt.Println("\nContinuing despite errors...")
}
if !disabledAny && len(errors) == 0 {
fmt.Println("\n✓ No conflicting display managers found")
} else if disabledAny && len(errors) == 0 {
fmt.Println("\n✓ Successfully handled all conflicting display managers")
}
return nil
}
func enableGreeter() error {
fmt.Println("=== DMS Greeter Enable ===")
fmt.Println()
configPath := "/etc/greetd/config.toml"
if _, err := os.Stat(configPath); os.IsNotExist(err) {
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)
}
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")
}
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
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nGreeter configuration verified and system state corrected.")
fmt.Println("To start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil
}
fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors()
if utils.CommandExists("sway") {
compositors = append(compositors, "sway")
}
if len(compositors) == 0 {
return fmt.Errorf("no supported compositors found (niri, Hyprland, or sway required)")
}
var selectedCompositor string
if len(compositors) == 1 {
selectedCompositor = compositors[0]
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
} else {
var err error
selectedCompositor, err = promptCompositorChoice(compositors)
if err != nil {
return err
}
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
}
greeterPathForConfig := ""
if !greeter.IsGreeterPackaged() {
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err)
}
greeterPathForConfig = dmsPath
}
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
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil
}
func isGreeterEnabled() bool {
command := readDefaultSessionCommand("/etc/greetd/config.toml")
return command != "" && strings.Contains(command, "dms-greeter")
}
func detectConfiguredCompositor() string {
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 {
return ""
}
inDefaultSession := false
for line := range strings.SplitSeq(string(data), "\n") {
if section, ok := parseTomlSection(line); ok {
inDefaultSession = section == "default_session"
continue
}
if !inDefaultSession {
continue
}
trimmed := stripTomlComment(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
}
}
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 {
return "Install package: dms-greeter"
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Install package: dms-greeter"
}
switch config.Family {
case distros.FamilyDebian:
return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)"
case distros.FamilySUSE:
return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)"
case distros.FamilyUbuntu:
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
case distros.FamilyFedora:
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
default:
return "Run 'dms greeter install' to install greeter"
}
}
func isPackageOnlyGreeterDistro() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return false
}
return config.Family == distros.FamilyDebian ||
config.Family == distros.FamilySUSE ||
config.Family == distros.FamilyUbuntu ||
config.Family == distros.FamilyFedora ||
config.Family == distros.FamilyArch
}
func promptCompositorChoice(compositors []string) (string, error) {
fmt.Println("\nMultiple compositors detected:")
for i, comp := range compositors {
fmt.Printf("%d) %s\n", i+1, comp)
}
var response string
fmt.Print("Choose compositor for greeter: ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
choice := 0
fmt.Sscanf(response, "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return compositors[choice-1], nil
}
func checkGreeterStatus() error {
fmt.Println("=== DMS Greeter Status ===")
fmt.Println()
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
configPath := "/etc/greetd/config.toml"
configuredCommand := ""
allGood := true
fmt.Println("Greeter Configuration:")
if _, err := os.ReadFile(configPath); err == nil {
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 {
case "niri":
fmt.Println(" Compositor: niri")
case "hyprland":
fmt.Println(" Compositor: Hyprland")
case "sway":
fmt.Println(" Compositor: sway")
default:
fmt.Println(" Compositor: unknown")
}
} 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:")
groupsCmd := exec.Command("groups", currentUser.Username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return fmt.Errorf("failed to check groups: %w", err)
}
greeterGroup := greeter.DetectGreeterGroup()
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
if inGreeterGroup {
fmt.Printf(" ✓ User is in %s group\n", greeterGroup)
} else {
fmt.Printf(" ✗ User is NOT in %s group\n", greeterGroup)
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
}
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 {
fmt.Printf(" ✗ %s not found\n", cacheDir)
fmt.Printf(" %s\n", packageInstallHint())
return nil
}
fmt.Println("\nConfiguration Symlinks:")
symlinks := []struct {
source string
target string
desc string
}{
{
source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"),
target: filepath.Join(cacheDir, "settings.json"),
desc: "Settings",
},
{
source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"),
target: filepath.Join(cacheDir, "session.json"),
desc: "Session state",
},
{
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
target: filepath.Join(cacheDir, "colors.json"),
desc: "Color theme",
},
}
for _, link := range symlinks {
targetInfo, err := os.Lstat(link.target)
if err != nil {
fmt.Printf(" ✗ %s: symlink not found at %s\n", link.desc, link.target)
allGood = false
continue
}
if targetInfo.Mode()&os.ModeSymlink == 0 {
fmt.Printf(" ✗ %s: %s is not a symlink\n", link.desc, link.target)
allGood = false
continue
}
linkDest, err := os.Readlink(link.target)
if err != nil {
fmt.Printf(" ✗ %s: failed to read symlink\n", link.desc)
allGood = false
continue
}
if linkDest != link.source {
fmt.Printf(" ✗ %s: symlink points to wrong location\n", link.desc)
fmt.Printf(" Expected: %s\n", link.source)
fmt.Printf(" Got: %s\n", linkDest)
allGood = false
continue
}
if _, err := os.Stat(link.source); os.IsNotExist(err) {
fmt.Printf(" ⚠ %s: symlink OK, but source file doesn't exist yet\n", link.desc)
fmt.Printf(" Will be created when you run DMS\n")
continue
}
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 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
}