From 9f38a47a0204157ff98a4367a832c6120dd4af5b Mon Sep 17 00:00:00 2001 From: purian23 Date: Mon, 23 Feb 2026 15:36:17 -0500 Subject: [PATCH] dms-greeter: Update dankinstall greeter automation w/distro packages --- core/cmd/dms/main_distro.go | 2 +- core/internal/distros/arch.go | 2 +- core/internal/distros/base.go | 13 ++ core/internal/distros/debian.go | 2 +- core/internal/distros/fedora.go | 2 +- core/internal/distros/interface.go | 1 + core/internal/distros/opensuse.go | 2 +- core/internal/distros/ubuntu.go | 2 +- core/internal/greeter/installer.go | 236 ++++++++++++++++++++++-- core/internal/tui/views_dependencies.go | 52 +++++- 10 files changed, 289 insertions(+), 25 deletions(-) diff --git a/core/cmd/dms/main_distro.go b/core/cmd/dms/main_distro.go index 6b7bcdef..a67026cf 100644 --- a/core/cmd/dms/main_distro.go +++ b/core/cmd/dms/main_distro.go @@ -18,7 +18,7 @@ func init() { runCmd.Flags().MarkHidden("daemon-child") // Add subcommands to greeter - greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) + greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) // Add subcommands to setup setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) diff --git a/core/internal/distros/arch.go b/core/internal/distros/arch.go index 9b2c1087..78192f05 100644 --- a/core/internal/distros/arch.go +++ b/core/internal/distros/arch.go @@ -126,7 +126,7 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency { } func (a *ArchDistribution) detectDMSGreeter() deps.Dependency { - return a.detectPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git")) + return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git")) } func (a *ArchDistribution) packageInstalled(pkg string) bool { diff --git a/core/internal/distros/base.go b/core/internal/distros/base.go index d8a4724d..63555c89 100644 --- a/core/internal/distros/base.go +++ b/core/internal/distros/base.go @@ -102,6 +102,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo } } +func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency { + status := deps.StatusMissing + if installed { + status = deps.StatusInstalled + } + return deps.Dependency{ + Name: name, + Status: status, + Description: description, + Required: false, + } +} + func (b *BaseDistribution) detectGit() deps.Dependency { return b.detectCommand("git", "Version control system") } diff --git a/core/internal/distros/debian.go b/core/internal/distros/debian.go index 5748bc7e..13d90987 100644 --- a/core/internal/distros/debian.go +++ b/core/internal/distros/debian.go @@ -88,7 +88,7 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency { } func (d *DebianDistribution) detectDMSGreeter() deps.Dependency { - return d.detectPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter")) + return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter")) } func (d *DebianDistribution) packageInstalled(pkg string) bool { diff --git a/core/internal/distros/fedora.go b/core/internal/distros/fedora.go index d2574290..42140bed 100644 --- a/core/internal/distros/fedora.go +++ b/core/internal/distros/fedora.go @@ -197,7 +197,7 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency { } func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency { - return f.detectPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter")) + return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter")) } func (f *FedoraDistribution) getPrerequisites() []string { diff --git a/core/internal/distros/interface.go b/core/internal/distros/interface.go index 8d9e66f1..f719f1bb 100644 --- a/core/internal/distros/interface.go +++ b/core/internal/distros/interface.go @@ -55,6 +55,7 @@ const ( PhaseAURPackages PhaseCursorTheme PhaseConfiguration + PhaseGreeterSetup PhaseComplete ) diff --git a/core/internal/distros/opensuse.go b/core/internal/distros/opensuse.go index 5b7927f5..6420a0c5 100644 --- a/core/internal/distros/opensuse.go +++ b/core/internal/distros/opensuse.go @@ -102,7 +102,7 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { } func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency { - return o.detectPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter")) + return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter")) } func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { diff --git a/core/internal/distros/ubuntu.go b/core/internal/distros/ubuntu.go index 6bd9f40f..c5fb09ea 100644 --- a/core/internal/distros/ubuntu.go +++ b/core/internal/distros/ubuntu.go @@ -96,7 +96,7 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency { } func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency { - return u.detectPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter")) + return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter")) } func (u *UbuntuDistribution) packageInstalled(pkg string) bool { diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index e2b8e2b4..098a1660 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -17,7 +17,6 @@ import ( "github.com/sblinch/kdl-go/document" ) -// DetectDMSPath checks for DMS installation following XDG Base Directory specification func DetectDMSPath() (string, error) { return config.LocateDMSConfig() } @@ -37,7 +36,6 @@ func DetectGreeterGroup() string { return "greeter" } -// DetectCompositors checks which compositors are installed func DetectCompositors() []string { var compositors []string @@ -51,7 +49,6 @@ func DetectCompositors() []string { return compositors } -// PromptCompositorChoice asks user to choose between compositors func PromptCompositorChoice(compositors []string) (string, error) { fmt.Println("\nMultiple compositors detected:") for i, comp := range compositors { @@ -292,11 +289,9 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { - // Check if dms-greeter is already in PATH if utils.CommandExists("dms-greeter") { logFunc("✓ dms-greeter wrapper already installed") } else { - // Install the wrapper script assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets") wrapperSrc := filepath.Join(assetsDir, "dms-greeter") @@ -333,7 +328,6 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass } } - // Create cache directory with proper permissions cacheDir := "/var/cache/dms-greeter" if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil { return fmt.Errorf("failed to create cache directory: %w", err) @@ -354,14 +348,90 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass return nil } +// EnsureACLInstalled installs the acl package (setfacl/getfacl) if not already present +func EnsureACLInstalled(logFunc func(string), sudoPassword string) error { + if utils.CommandExists("setfacl") { + return nil + } + + logFunc("setfacl not found – installing acl package...") + + osInfo, err := distros.GetOSInfo() + if err != nil { + return fmt.Errorf("failed to detect OS: %w", err) + } + + config, exists := distros.Registry[osInfo.Distribution.ID] + if !exists { + return fmt.Errorf("unsupported distribution for automatic acl installation: %s", osInfo.Distribution.ID) + } + + ctx := context.Background() + var installCmd *exec.Cmd + + switch config.Family { + case distros.FamilyArch: + if sudoPassword != "" { + installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl") + } else { + installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl") + } + + case distros.FamilyFedora: + if sudoPassword != "" { + installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl") + } else { + installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl") + } + + case distros.FamilySUSE: + if sudoPassword != "" { + installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl") + } else { + installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl") + } + + case distros.FamilyUbuntu, distros.FamilyDebian: + if sudoPassword != "" { + installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl") + } else { + installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl") + } + + case distros.FamilyGentoo: + if sudoPassword != "" { + installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl") + } else { + installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl") + } + + case distros.FamilyNix: + return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix") + + default: + return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family) + } + + installCmd.Stdout = os.Stdout + installCmd.Stderr = os.Stderr + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install acl: %w", err) + } + + logFunc("✓ acl package installed") + return nil +} + // SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { + if err := EnsureACLInstalled(logFunc, sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: could not install acl package: %v", err)) + logFunc(" ACL permissions will be skipped; theme sync may not work correctly.") + return nil + } if !utils.CommandExists("setfacl") { - logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.") - logFunc(" If theme sync doesn't work, you may need to install acl package:") - logFunc(" - Fedora/RHEL: sudo dnf install acl") - logFunc(" - Debian/Ubuntu: sudo apt-get install acl") - logFunc(" - Arch: sudo pacman -S acl") + // setfacl still not found after install attempt (e.g. unsupported filesystem) + logFunc("⚠ Warning: setfacl still not available after install attempt; skipping ACL setup.") return nil } @@ -394,7 +464,6 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { } } - // Set ACL to allow greeter user read+execute permission (for session discovery) if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil { logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path)) @@ -429,7 +498,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error { if err == nil && strings.Contains(string(groupsOutput), group) { logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group)) } else { - // Add current user to greeter group for file access permissions if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil { return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err) } @@ -469,7 +537,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error { logFunc(fmt.Sprintf("✓ Set group permissions for %s", dir.desc)) } - // Set up ACLs on parent directories to allow greeter user traversal if err := SetupParentDirectoryACLs(logFunc, sudoPassword); err != nil { return fmt.Errorf("failed to setup parent directory ACLs: %w", err) } @@ -917,14 +984,12 @@ user = "%s" } } - // Determine wrapper command path + // If dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter wrapperCmd := "dms-greeter" if !utils.CommandExists("dms-greeter") { wrapperCmd = "/usr/local/bin/dms-greeter" } - // Build command based on compositor and dms path - // When dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter compositorLower := strings.ToLower(compositor) var command string if dmsPath == "" { @@ -1068,3 +1133,140 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error { cmd.Stderr = os.Stderr return cmd.Run() } + +func checkSystemdEnabled(service string) (string, error) { + cmd := exec.Command("systemctl", "is-enabled", service) + output, _ := cmd.Output() + return strings.TrimSpace(string(output)), nil +} + +func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error { + conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} + for _, dm := range conflictingDMs { + state, err := checkSystemdEnabled(dm) + if err != nil || state == "" || state == "not-found" { + continue + } + switch state { + case "enabled", "enabled-runtime", "static", "indirect", "alias": + logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm)) + if err := runSudoCmd(sudoPassword, "systemctl", "disable", "--now", dm); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err)) + } else { + logFunc(fmt.Sprintf("✓ Disabled %s", dm)) + } + } + } + return nil +} + +// EnableGreetd unmasks and enables greetd, forcing it over any other DM. +func EnableGreetd(sudoPassword string, logFunc func(string)) error { + state, err := checkSystemdEnabled("greetd") + if err != nil { + return fmt.Errorf("failed to check greetd state: %w", err) + } + if state == "not-found" { + return fmt.Errorf("greetd service not found; ensure greetd is installed") + } + if state == "masked" || state == "masked-runtime" { + logFunc(" Unmasking greetd...") + if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil { + return fmt.Errorf("failed to unmask greetd: %w", err) + } + logFunc(" ✓ Unmasked greetd") + } + logFunc(" Enabling greetd service (--force)...") + if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil { + return fmt.Errorf("failed to enable greetd: %w", err) + } + logFunc("✓ greetd enabled") + return nil +} + +func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error { + cmd := exec.Command("systemctl", "get-default") + output, err := cmd.Output() + if err != nil { + logFunc(fmt.Sprintf("⚠ Warning: could not get default systemd target: %v", err)) + return nil + } + current := strings.TrimSpace(string(output)) + if current == "graphical.target" { + logFunc("✓ Default target is already graphical.target") + return nil + } + logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current)) + if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil { + return fmt.Errorf("failed to set graphical target: %w", err) + } + logFunc("✓ Default target set to graphical.target") + return nil +} + +// AutoSetupGreeter performs the full non-interactive greeter setup +func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) error { + if IsGreeterPackaged() && 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") + } + + logFunc("Ensuring greetd is installed...") + if err := EnsureGreetdInstalled(logFunc, sudoPassword); err != nil { + return fmt.Errorf("greetd install failed: %w", err) + } + + dmsPath := "" + if !IsGreeterPackaged() { + detected, err := DetectDMSPath() + if err != nil { + return fmt.Errorf("DMS installation not found: %w", err) + } + dmsPath = detected + logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath)) + } else { + logFunc("✓ Using packaged dms-greeter (/usr/share/quickshell/dms-greeter)") + } + + logFunc("Setting up dms-greeter group and permissions...") + if err := SetupDMSGroup(logFunc, sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: group/permissions setup error: %v", err)) + } + + logFunc("Copying greeter files...") + if err := CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil { + return fmt.Errorf("failed to copy greeter files: %w", err) + } + + logFunc("Configuring greetd...") + greeterPathForConfig := "" + if !IsGreeterPackaged() { + greeterPathForConfig = dmsPath + } + if err := ConfigureGreetd(greeterPathForConfig, compositor, logFunc, sudoPassword); err != nil { + return fmt.Errorf("failed to configure greetd: %w", err) + } + + logFunc("Synchronizing DMS configurations...") + if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err)) + } + + logFunc("Checking for conflicting display managers...") + if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: %v", err)) + } + + logFunc("Enabling greetd service...") + if err := EnableGreetd(sudoPassword, logFunc); err != nil { + return fmt.Errorf("failed to enable greetd: %w", err) + } + + logFunc("Ensuring graphical.target as default...") + if err := EnsureGraphicalTarget(sudoPassword, logFunc); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: %v", err)) + } + + logFunc("✓ DMS greeter setup complete") + return nil +} diff --git a/core/internal/tui/views_dependencies.go b/core/internal/tui/views_dependencies.go index f1b112e0..f55b6a10 100644 --- a/core/internal/tui/views_dependencies.go +++ b/core/internal/tui/views_dependencies.go @@ -7,6 +7,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" + "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" tea "github.com/charmbracelet/bubbletea" ) @@ -80,19 +81,24 @@ func (m Model) viewDependencyReview() string { } } + note := "" + if dep.Name == "dms-greeter" { + note = m.styles.Subtle.Render(" (selection replaces your current display manager)") + } + var line string if i == m.selectedDep { line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status) if dep.Version != "" { line += fmt.Sprintf(" (%s)", dep.Version) } - line = m.styles.SelectedOption.Render(line) + line = m.styles.SelectedOption.Render(line) + note } else { line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status) if dep.Version != "" { line += fmt.Sprintf(" (%s)", dep.Version) } - line = m.styles.Normal.Render(line) + line = m.styles.Normal.Render(line) + note } b.WriteString(line) @@ -115,6 +121,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) { m.state = StateError } else { m.dependencies = depsMsg.deps + // dms-greeter is opt-in skipped by default + for _, dep := range depsMsg.deps { + if dep.Name == "dms-greeter" { + m.disabledItems["dms-greeter"] = true + break + } + } m.state = StateDependencyReview } return m, m.listenForLogs() @@ -230,6 +243,41 @@ func (m Model) installPackages() tea.Cmd { // Convert installer messages to TUI messages go func() { for msg := range installerProgressChan { + // Run optional greeter setup + if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil { + greeterSelected := false + for _, dep := range m.dependencies { + if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] { + greeterSelected = true + break + } + } + if greeterSelected { + compositorName := "niri" + if m.selectedWM == 1 { + compositorName = "Hyprland" + } + m.packageProgressChan <- packageInstallProgressMsg{ + progress: 0.92, + step: "Configuring DMS greeter...", + logOutput: "Starting automated greeter setup...", + } + greeterLogFunc := func(line string) { + m.packageProgressChan <- packageInstallProgressMsg{ + progress: 0.94, + step: "Configuring DMS greeter...", + logOutput: line, + } + } + if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil { + m.packageProgressChan <- packageInstallProgressMsg{ + progress: 0.96, + step: "Greeter setup warning", + logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err), + } + } + } + } tuiMsg := packageInstallProgressMsg{ progress: msg.Progress, step: msg.Step,