mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42:05 -04:00
- 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.
1267 lines
39 KiB
Go
1267 lines
39 KiB
Go
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",
|
||
PreRunE: requireMutableSystemCommand,
|
||
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",
|
||
PreRunE: requireMutableSystemCommand,
|
||
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",
|
||
PreRunE: requireMutableSystemCommand,
|
||
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
|
||
}
|