diff --git a/core/README.md b/core/README.md index fed5bfad6..1a725ed8a 100644 --- a/core/README.md +++ b/core/README.md @@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in Command-line interface and daemon for shell management and system control. **dankinstall** -Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags. +Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags. ## System Integration @@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory ## Supported Distributions -Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives) +Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives) **Arch Linux** Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency. @@ -214,4 +214,7 @@ Most packages available in standard repos. Minimal building required. **Gentoo** Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration. +**Void Linux** +Uses XBPS with the DMS and DankLinux self-hosted repositories. + See installer output for distribution-specific details during installation. diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index 2a002f3c6..a98976fe4 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -1534,6 +1534,8 @@ func packageInstallHint() string { 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'" + case distros.FamilyVoid: + return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf)" default: return "Run 'dms greeter install' to install greeter" } @@ -1572,7 +1574,8 @@ func isPackageOnlyGreeterDistro() bool { config.Family == distros.FamilySUSE || config.Family == distros.FamilyUbuntu || config.Family == distros.FamilyFedora || - config.Family == distros.FamilyArch + config.Family == distros.FamilyArch || + config.Family == distros.FamilyVoid } func promptCompositorChoice(compositors []string) (string, error) { diff --git a/core/cmd/dms/commands_setup.go b/core/cmd/dms/commands_setup.go index aec2fbff1..d6a9b6856 100644 --- a/core/cmd/dms/commands_setup.go +++ b/core/cmd/dms/commands_setup.go @@ -10,6 +10,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "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/privesc" @@ -298,6 +299,9 @@ func runSetup() error { if wmSelected { if wm == deps.WindowManagerMango { useSystemd = false + } else if isVoidSetup() { + useSystemd = false + fmt.Println("\nVoid Linux detected; deploying non-systemd session config.") } else { useSystemd = promptSystemd() } @@ -372,6 +376,15 @@ func runSetup() error { return nil } +func isVoidSetup() bool { + osInfo, err := distros.GetOSInfo() + if err != nil { + return false + } + config, exists := distros.Registry[osInfo.Distribution.ID] + return exists && config.Family == distros.FamilyVoid +} + // Add user to the input group for the evdev manager for inut state tracking. // Caps Lock OSD and the Caps Lock bar indicator. func ensureInputGroup() { diff --git a/core/internal/config/deployer.go b/core/internal/config/deployer.go index 82c1bcbf9..0f99ced74 100644 --- a/core/internal/config/deployer.go +++ b/core/internal/config/deployer.go @@ -61,6 +61,10 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true) } +func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) { + return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd) +} + func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) { var results []DeploymentResult diff --git a/core/internal/config/embedded/ghostty.conf b/core/internal/config/embedded/ghostty.conf index 8ca05a29e..5b3229673 100644 --- a/core/internal/config/embedded/ghostty.conf +++ b/core/internal/config/embedded/ghostty.conf @@ -20,8 +20,8 @@ mouse-hide-while-typing = true copy-on-select = false confirm-close-surface = false -# Disable annoying copied to clipboard -app-notifications = no-clipboard-copy,no-config-reload +# Disable in-app Ghostty toast notifications +app-notifications = false # Key bindings for common actions #keybind = ctrl+c=copy_to_clipboard diff --git a/core/internal/distros/interface.go b/core/internal/distros/interface.go index f719f1bb1..2b70f20bf 100644 --- a/core/internal/distros/interface.go +++ b/core/internal/distros/interface.go @@ -17,6 +17,7 @@ const ( FamilyDebian DistroFamily = "debian" FamilyNix DistroFamily = "nix" FamilyGentoo DistroFamily = "gentoo" + FamilyVoid DistroFamily = "void" ) // PackageManagerType defines the package manager a distro uses @@ -29,6 +30,7 @@ const ( PackageManagerZypper PackageManagerType = "zypper" PackageManagerNix PackageManagerType = "nix" PackageManagerPortage PackageManagerType = "portage" + PackageManagerXBPS PackageManagerType = "xbps" ) // RepositoryType defines the type of repository for a package @@ -42,6 +44,7 @@ const ( RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE) RepoTypeFlake RepositoryType = "flake" // Nix flake RepoTypeGURU RepositoryType = "guru" // Gentoo GURU + RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository RepoTypeManual RepositoryType = "manual" // Manual build from source ) diff --git a/core/internal/distros/void.go b/core/internal/distros/void.go new file mode 100644 index 000000000..41ddd0759 --- /dev/null +++ b/core/internal/distros/void.go @@ -0,0 +1,530 @@ +package distros + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" +) + +const ( + VoidDMSRepo = "https://avengemedia.github.io/DankMaterialShell/current" + VoidDankLinuxRepo = "https://avengemedia.github.io/DankLinux/current" + VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64" + + voidRunitSvDir = "/etc/sv" + voidRunitServiceDir = "/var/service" +) + +func init() { + Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution { + return NewVoidDistribution(config, logChan) + }) +} + +type VoidDistribution struct { + *BaseDistribution + config DistroConfig +} + +func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution { + return &VoidDistribution{ + BaseDistribution: NewBaseDistribution(logChan), + config: config, + } +} + +func (v *VoidDistribution) GetID() string { + return v.config.ID +} + +func (v *VoidDistribution) GetColorHex() string { + return v.config.ColorHex +} + +func (v *VoidDistribution) GetFamily() DistroFamily { + return v.config.Family +} + +func (v *VoidDistribution) GetPackageManager() PackageManagerType { + return PackageManagerXBPS +} + +func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) { + var dependencies []deps.Dependency + + dependencies = append(dependencies, v.detectDMS()) + dependencies = append(dependencies, v.detectSpecificTerminal(terminal)) + dependencies = append(dependencies, v.detectGit()) + dependencies = append(dependencies, v.detectWindowManager(wm)) + dependencies = append(dependencies, v.detectQuickshell()) + dependencies = append(dependencies, v.detectDMSGreeter()) + dependencies = append(dependencies, v.detectXDGPortal()) + dependencies = append(dependencies, v.detectAccountsService()) + dependencies = append(dependencies, v.detectDBus()) + dependencies = append(dependencies, v.detectElogind()) + + if wm == deps.WindowManagerHyprland { + dependencies = append(dependencies, v.detectHyprlandTools()...) + } + + if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango { + dependencies = append(dependencies, v.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, v.detectMatugen()) + dependencies = append(dependencies, v.detectDgop()) + + return dependencies, nil +} + +func (v *VoidDistribution) detectDMS() deps.Dependency { + status := deps.StatusMissing + version := "" + variant := deps.VariantStable + + if v.packageInstalled("dms-git") { + status = deps.StatusInstalled + version = v.packageVersion("dms-git") + variant = deps.VariantGit + } else if v.packageInstalled("dms") { + status = deps.StatusInstalled + version = v.packageVersion("dms") + } else if v.commandExists("dms") { + status = deps.StatusInstalled + } + + return deps.Dependency{ + Name: "dms (DankMaterialShell)", + Status: status, + Version: version, + Description: "Desktop Management System package", + Required: true, + Variant: variant, + CanToggle: true, + } +} + +func (v *VoidDistribution) detectQuickshell() deps.Dependency { + dep := v.BaseDistribution.detectQuickshell() + dep.CanToggle = false + return dep +} + +func (v *VoidDistribution) detectXDGPortal() deps.Dependency { + return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (v *VoidDistribution) detectDMSGreeter() deps.Dependency { + return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter")) +} + +func (v *VoidDistribution) detectAccountsService() deps.Dependency { + return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice")) +} + +func (v *VoidDistribution) detectDBus() deps.Dependency { + return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus")) +} + +func (v *VoidDistribution) detectElogind() deps.Dependency { + return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl")) +} + +func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency { + return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite")) +} + +func (v *VoidDistribution) packageInstalled(pkg string) bool { + return exec.Command("xbps-query", pkg).Run() == nil +} + +func (v *VoidDistribution) packageVersion(pkg string) string { + output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(output)) +} + +func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + packages := map[string]PackageMapping{ + "git": {Name: "git", Repository: RepoTypeSystem}, + "ghostty": {Name: "ghostty", Repository: RepoTypeSystem}, + "kitty": {Name: "kitty", Repository: RepoTypeSystem}, + "alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, + "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, + "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, + "dbus": {Name: "dbus", Repository: RepoTypeSystem}, + "elogind": {Name: "elogind", Repository: RepoTypeSystem}, + + "quickshell": {Name: "quickshell", Repository: RepoTypeSystem}, + "matugen": {Name: "matugen", Repository: RepoTypeSystem}, + "dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]), + "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}, + "dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo}, + } + + switch wm { + case deps.WindowManagerHyprland: + packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo} + packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo} + packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} + case deps.WindowManagerNiri: + packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem} + packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} + case deps.WindowManagerMango: + packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem} + packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} + } + + return packages +} + +func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantStable { + return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo} + } + return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo} +} + +func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Checking XBPS...", + IsComplete: false, + LogOutput: "Checking for xbps-install", + } + + if _, err := exec.LookPath("xbps-install"); err != nil { + return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err) + } + + return nil +} + +func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.05, + Step: "Checking system prerequisites...", + IsComplete: false, + LogOutput: "Starting prerequisite check...", + } + + if wm == deps.WindowManagerHyprland { + arch, err := v.xbpsArch(ctx) + if err != nil { + return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err) + } + if arch != "x86_64" { + return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch) + } + } + + if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + if len(xbpsPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.15, + Step: "Enabling DMS XBPS repositories...", + IsComplete: false, + NeedsSudo: true, + LogOutput: "Setting up custom XBPS repositories for DMS packages", + } + if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to enable XBPS repositories: %w", err) + } + } + + allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs)) + if len(allPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")), + } + if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install XBPS packages: %w", err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd") + if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to enable Void session services: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if !v.isRunitSystem() { + v.log("Void runit service directory not detected; skipping dbus/elogind service enablement") + return nil + } + + for _, service := range []string{"dbus", "elogind"} { + if !v.runitServiceInstalled(service) { + v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service)) + continue + } + if v.runitServiceEnabled(service) { + v.log(fmt.Sprintf("Void runit service %s already enabled", service)) + continue + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.92, + Step: fmt.Sprintf("Enabling %s runit service...", service), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)), + LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service), + } + + cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service))) + if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil { + return fmt.Errorf("failed to enable %s runit service: %w", service, err) + } + v.log(fmt.Sprintf("✓ Enabled %s runit service", service)) + } + + return nil +} + +func (v *VoidDistribution) isRunitSystem() bool { + if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() { + return true + } + if _, err := os.Stat("/run/systemd/system"); err == nil { + return false + } + if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() { + return true + } + return false +} + +func (v *VoidDistribution) runitServiceInstalled(name string) bool { + fi, err := os.Stat(filepath.Join(voidRunitSvDir, name)) + return err == nil && fi.IsDir() +} + +func (v *VoidDistribution) runitServiceEnabled(name string) bool { + _, err := os.Lstat(filepath.Join(voidRunitServiceDir, name)) + return err == nil +} + +func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) { + systemPkgs := []string{} + xbpsPkgs := []PackageMapping{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := v.GetPackageMappingWithVariants(wm, variantMap) + + for _, dep := range dependencies { + if disabledFlags[dep.Name] { + continue + } + + if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] { + continue + } + + pkgInfo, exists := packageMap[dep.Name] + if !exists { + v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) + continue + } + + switch pkgInfo.Repository { + case RepoTypeXBPS: + xbpsPkgs = append(xbpsPkgs, pkgInfo) + case RepoTypeSystem: + systemPkgs = append(systemPkgs, pkgInfo.Name) + } + } + + return systemPkgs, xbpsPkgs +} + +func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + enabledRepos := make(map[string]bool) + enabledRepoURLs := []string{} + + for _, pkg := range xbpsPkgs { + if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] { + continue + } + + repoName := v.xbpsRepoName(pkg.RepoURL) + confPath := filepath.Join("/etc/xbps.d", repoName+".conf") + repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL) + repoFileContent := repoLine + "\n" + + if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent { + v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL)) + enabledRepos[pkg.RepoURL] = true + enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL) + continue + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.18, + Step: fmt.Sprintf("Adding XBPS repo %s...", repoName), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath), + LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL), + } + + mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d") + if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil { + return fmt.Errorf("failed to create /etc/xbps.d: %w", err) + } + + writeCmd := privesc.ExecCommand(ctx, sudoPassword, + fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath)) + if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil { + return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err) + } + + enabledRepos[pkg.RepoURL] = true + enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL) + } + + if len(enabledRepos) > 0 { + syncArgs := []string{"xbps-install", "-Sy", "-i"} + for _, repoURL := range enabledRepoURLs { + syncArgs = append(syncArgs, "--repository", repoURL) + } + syncCommand := strings.Join(syncArgs, " ") + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.25, + Step: "Synchronizing XBPS repositories...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'", + LogOutput: "Synchronizing XBPS repository indexes", + } + + syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'") + if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil { + return fmt.Errorf("failed to synchronize XBPS repositories: %w", err) + } + } + + return nil +} + +func (v *VoidDistribution) xbpsRepoName(repoURL string) string { + switch repoURL { + case VoidDMSRepo: + return "dms" + case VoidDankLinuxRepo: + return "danklinux" + case VoidHyprlandRepo: + return "hyprland" + default: + name := strings.TrimPrefix(repoURL, "https://") + name = strings.TrimPrefix(name, "http://") + name = strings.NewReplacer("/", "-", ".", "-").Replace(name) + return name + } +} + +func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) { + output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil +} + +func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + args := append([]string{"xbps-install", "-Sy"}, packages...) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.40, + Step: "Installing XBPS packages...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " ")) + return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85) +} + +func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string { + names := make([]string, len(packages)) + for i, pkg := range packages { + names[i] = pkg.Name + } + return names +} + +func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string { + seen := make(map[string]bool) + var unique []string + for _, group := range groups { + for _, pkg := range group { + if pkg == "" || seen[pkg] { + continue + } + seen[pkg] = true + unique = append(unique, pkg) + } + } + return unique +} diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index b1f413ff2..491415868 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -30,6 +30,11 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter" const GreeterCacheDir = "/var/cache/dms-greeter" +const ( + runitSvDir = "/etc/sv" + runitServiceDir = "/var/service" +) + func DetectDMSPath() (string, error) { return config.LocateDMSConfig() } @@ -41,6 +46,96 @@ func IsNixOS() bool { return err == nil } +func IsVoidLinux() bool { + osInfo, err := distros.GetOSInfo() + if err != nil { + return false + } + config, exists := distros.Registry[osInfo.Distribution.ID] + return exists && config.Family == distros.FamilyVoid +} + +func isRunit() bool { + if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() { + return true + } + if _, err := os.Stat("/run/systemd/system"); err == nil { + return false + } + if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() { + return true + } + return false +} + +func runitServiceInstalled(name string) bool { + fi, err := os.Stat(filepath.Join(runitSvDir, name)) + return err == nil && fi.IsDir() +} + +func runitServiceEnabled(name string) bool { + _, err := os.Lstat(filepath.Join(runitServiceDir, name)) + return err == nil +} + +func enableRunitService(name, sudoPassword string) error { + if !runitServiceInstalled(name) { + return fmt.Errorf("runit service %q not found in %s", name, runitSvDir) + } + if runitServiceEnabled(name) { + return nil + } + return privesc.Run(context.Background(), sudoPassword, "ln", "-sf", + filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name)) +} + +func disableRunitService(name, sudoPassword string) error { + if !runitServiceEnabled(name) { + return nil + } + return privesc.Run(context.Background(), sudoPassword, "rm", "-f", + filepath.Join(runitServiceDir, name)) +} + +func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) { + if runitServiceInstalled("seatd") { + if err := enableRunitService("seatd", sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err)) + } else { + logFunc("✓ seatd enabled") + } + } else { + logFunc("⚠ seatd not installed — the greeter compositor needs it for GPU/seat access") + } + + if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil { + logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err)) + } else { + logFunc(fmt.Sprintf("✓ %s added to seat groups (_seatd, video, input)", greeterUser)) + } +} + +func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) { + const pamPath = "/etc/pam.d/greetd" + data, err := os.ReadFile(pamPath) + if err != nil { + logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err)) + return + } + if strings.Contains(string(data), "pam_rundir") { + logFunc("✓ pam_rundir already present in greetd PAM") + return + } + + line := "session optional pam_rundir.so" + if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c", + fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil { + logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err)) + return + } + logFunc("✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)") +} + func DetectGreeterGroup() string { data, err := os.ReadFile("/etc/group") if err != nil { @@ -766,6 +861,8 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error { installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd") case distros.FamilyGentoo: installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd") + case distros.FamilyVoid: + installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy greetd") case distros.FamilyNix: return fmt.Errorf("on NixOS, please add greetd to your configuration.nix") default: @@ -892,6 +989,14 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { } failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper) installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git") + case distros.FamilyVoid: + failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter" + logFunc("Adding DMS XBPS repository...") + if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil { + logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err)) + } + privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run() + installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter") default: return false } @@ -909,6 +1014,20 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { return true } +func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error { + confPath := filepath.Join("/etc/xbps.d", name+".conf") + repoLine := fmt.Sprintf("repository=%s", repoURL) + repoFileContent := repoLine + "\n" + if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent { + return nil + } + if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil { + return err + } + return privesc.Run(ctx, sudoPassword, "sh", "-c", + fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath)) +} + // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { if IsGreeterPackaged() { @@ -2275,6 +2394,19 @@ func checkSystemdEnabled(service string) (string, error) { func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error { conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} for _, dm := range conflictingDMs { + if isRunit() { + if !runitServiceEnabled(dm) { + continue + } + logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm)) + if err := disableRunitService(dm, sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err)) + } else { + logFunc(fmt.Sprintf("✓ Disabled %s", dm)) + } + continue + } + state, err := checkSystemdEnabled(dm) if err != nil || state == "" || state == "not-found" { continue @@ -2294,6 +2426,19 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string) // EnableGreetd unmasks and enables greetd, forcing it over any other DM. func EnableGreetd(sudoPassword string, logFunc func(string)) error { + if isRunit() { + if !runitServiceInstalled("greetd") { + return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir) + } + ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc) + ensureGreetdPamRundir(sudoPassword, logFunc) + if err := enableRunitService("greetd", sudoPassword); err != nil { + return fmt.Errorf("failed to enable greetd: %w", err) + } + logFunc(fmt.Sprintf("✓ greetd enabled (%s)", runitServiceDir)) + return nil + } + state, err := checkSystemdEnabled("greetd") if err != nil { return fmt.Errorf("failed to check greetd state: %w", err) @@ -2317,6 +2462,11 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error { } func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error { + if isRunit() { + logFunc("✓ runit detected; no graphical target is needed") + return nil + } + cmd := exec.Command("systemctl", "get-default") output, err := cmd.Output() if err != nil { diff --git a/core/internal/headless/runner.go b/core/internal/headless/runner.go index efb1550b9..beaf71c42 100644 --- a/core/internal/headless/runner.go +++ b/core/internal/headless/runner.go @@ -236,13 +236,18 @@ func (r *Runner) Run() error { r.log("Starting configuration deployment") deployer := config.NewConfigDeployer(r.logChan) - results, err := deployer.DeployConfigurationsSelectiveWithReinstalls( + useSystemd := true + if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid { + useSystemd = false + } + results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd( context.Background(), wm, terminal, dependencies, replaceConfigs, reinstallItems, + useSystemd, ) if err != nil { return fmt.Errorf("configuration deployment failed: %w", err) diff --git a/core/internal/tui/views_config.go b/core/internal/tui/views_config.go index 0edadde4b..e5cd60901 100644 --- a/core/internal/tui/views_config.go +++ b/core/internal/tui/views_config.go @@ -9,6 +9,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" tea "github.com/charmbracelet/bubbletea" ) @@ -129,7 +130,7 @@ func (m Model) deployConfigurations() tea.Cmd { deployer := config.NewConfigDeployer(m.logChan) - results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems) + results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig()) return configDeploymentResult{ results: results, @@ -138,6 +139,17 @@ func (m Model) deployConfigurations() tea.Cmd { } } +func (m Model) useSystemdConfig() bool { + if m.osInfo == nil { + return true + } + distroConfig, exists := distros.Registry[m.osInfo.Distribution.ID] + if !exists { + return true + } + return distroConfig.Family != distros.FamilyVoid +} + func (m Model) viewConfigConfirmation() string { var b strings.Builder @@ -195,12 +207,50 @@ func (m Model) viewConfigConfirmation() string { b.WriteString(backup) b.WriteString("\n\n") + if note := m.configReplacementNote(); note != "" { + b.WriteString(m.styles.Subtle.Render(note)) + b.WriteString("\n\n") + } + help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue") b.WriteString(help) return b.String() } +func (m Model) configReplacementNote() string { + if m.selectedConfig < 0 || m.selectedConfig >= len(m.existingConfigs) { + return "" + } + configInfo := m.existingConfigs[m.selectedConfig] + if !configInfo.Exists { + return "" + } + + switch configInfo.ConfigType { + case "Niri": + if m.useSystemdConfig() { + return "Replacing Niri writes the DMS Niri template and uses the user systemd dms service for shell autostart." + } + return `Replacing Niri writes the DMS Niri template and starts DMS from Niri with spawn-at-startup "dms" "run".` + case "Hyprland": + if m.useSystemdConfig() { + return "Replacing Hyprland writes the DMS Lua template and uses the user systemd dms service for shell autostart." + } + return `Replacing Hyprland writes the DMS Lua template and starts DMS from Hyprland with hl.exec_cmd("dms run").` + case "Mango": + return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec-once=dms run." + case "Ghostty": + return "Replacing Ghostty writes the DMS terminal defaults and theme include." + case "Kitty": + return "Replacing Kitty writes the DMS terminal defaults, theme include, and tab styling." + case "Alacritty": + return "Replacing Alacritty writes the DMS terminal defaults and theme import." + default: + return "" + } +} + func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) { if result, ok := msg.(configCheckResult); ok { if result.error != nil { diff --git a/core/internal/tui/views_install.go b/core/internal/tui/views_install.go index ad04aac36..9d4c80d14 100644 --- a/core/internal/tui/views_install.go +++ b/core/internal/tui/views_install.go @@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string { return "dms-shell-git" } return "dms-shell" - case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE: + case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid: if isGit { return "dms-git" } @@ -168,6 +168,8 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string { return "sudo apt remove " + pkg case distros.FamilySUSE: return "sudo zypper remove " + pkg + case distros.FamilyVoid: + return "sudo xbps-remove -R " + pkg default: return "" } @@ -201,15 +203,26 @@ func (m Model) viewInstallComplete() string { wm := m.selectedWindowManager() - // mango launches DMS via `exec-once=dms run` (not a systemd session target) loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\"" - switch wm { - case deps.WindowManagerNiri: - loginHint = "If you do not have a greeter, login with \"niri-session\"" - case deps.WindowManagerHyprland: - loginHint = "If you do not have a greeter, login with \"Hyprland\"" - case deps.WindowManagerMango: - loginHint = "If you do not have a greeter, login with \"mango\"" + if !m.useSystemdConfig() { + switch wm { + case deps.WindowManagerNiri: + loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri" + case deps.WindowManagerHyprland: + loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland" + case deps.WindowManagerMango: + loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session mango" + } + } else { + // mango launches DMS via `exec-once=dms run` (not a systemd session target) + switch wm { + case deps.WindowManagerNiri: + loginHint = "If you do not have a greeter, login with \"niri-session\"" + case deps.WindowManagerHyprland: + loginHint = "If you do not have a greeter, login with \"Hyprland\"" + case deps.WindowManagerMango: + loginHint = "If you do not have a greeter, login with \"mango\"" + } } b.WriteString("\n") @@ -222,7 +235,17 @@ func (m Model) viewInstallComplete() string { labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle)) b.WriteString(labelStyle.Render("Troubleshooting:") + "\n") - if wm == deps.WindowManagerMango { + if !m.useSystemdConfig() { + switch wm { + case deps.WindowManagerNiri: + b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`) + "\n") + case deps.WindowManagerHyprland: + b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`) + "\n") + case deps.WindowManagerMango: + b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n") + } + b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("quickshell --path ~/.config/quickshell/dms log") + "\n") + } else if wm == deps.WindowManagerMango { b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n") b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n") } else { diff --git a/distro/void/README.md b/distro/void/README.md index 094ad52b2..69d1b305d 100644 --- a/distro/void/README.md +++ b/distro/void/README.md @@ -52,8 +52,8 @@ checkout at `srcpkgs//template` to build or submit it. ## Dependencies Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`, -and `matugen` (which drives the Material You theming). The rest are optional — -install whichever features you want: +`matugen` (which drives the Material You theming), `dbus`, and `elogind`. +The rest are optional, install whichever features you want: | Package | Enables | | --- | --- | @@ -98,6 +98,27 @@ spawn-at-startup "dms" "run" or Hyprland: `exec-once = dms run`. +From a TTY on Void without a greeter, start your compositor through a D-Bus +session: + +```sh +dbus-run-session niri +dbus-run-session Hyprland +dbus-run-session mango +``` + +The `mangowc` package provides the `mango` command. + +For power menu actions to work on runit systems, make sure the system D-Bus and +elogind services are enabled: + +```sh +sudo ln -sf /etc/sv/dbus /var/service/dbus +sudo ln -sf /etc/sv/elogind /var/service/elogind +``` + +The `dankinstall` Void path does this automatically after installing packages. + ## Greeter (optional) Install `dms-greeter`, then let the CLI do the setup: diff --git a/distro/void/srcpkgs/dms-git/template b/distro/void/srcpkgs/dms-git/template index fe1f4cc4e..f2a82362b 100644 --- a/distro/void/srcpkgs/dms-git/template +++ b/distro/void/srcpkgs/dms-git/template @@ -32,13 +32,12 @@ conflicts="dms" provides="dms-${version}_${revision}" # Optional feature deps are listed in distro/void/README.md. -depends="quickshell accountsservice dgop matugen dbus" +depends="quickshell accountsservice dgop matugen dbus elogind" post_install() { # QML shell tree (build_style=go already installed the dms binary) vmkdir usr/share/quickshell/dms vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms - echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION" # Desktop entry + icon vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications diff --git a/distro/void/srcpkgs/dms/template b/distro/void/srcpkgs/dms/template index dbcfd891a..fca99409f 100644 --- a/distro/void/srcpkgs/dms/template +++ b/distro/void/srcpkgs/dms/template @@ -22,13 +22,12 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$ checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61 # Optional feature deps are listed in distro/void/README.md. -depends="quickshell accountsservice dgop matugen dbus" +depends="quickshell accountsservice dgop matugen dbus elogind" post_install() { # QML shell tree (build_style=go already installed the dms binary) vmkdir usr/share/quickshell/dms vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms - echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION" # Desktop entry + icon vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index f924e0889..08f898435 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -14,6 +14,8 @@ Singleton { property bool hasUwsm: false property bool isElogind: false + property bool loginctlCommandAvailable: false + property bool systemctlCommandAvailable: false property bool hibernateSupported: false property bool inhibitorAvailable: true property bool idleInhibited: false @@ -52,6 +54,8 @@ Singleton { repeat: false onTriggered: { detectElogindProcess.running = true; + detectLoginctlProcess.running = true; + detectSystemctlProcess.running = true; detectHibernateProcess.running = true; detectPrimeRunProcess.running = true; if (!SettingsData.loginctlLockIntegration) { @@ -87,6 +91,26 @@ Singleton { } } + Process { + id: detectLoginctlProcess + running: false + command: ["sh", "-c", "command -v loginctl"] + + onExited: function (exitCode) { + loginctlCommandAvailable = (exitCode === 0); + } + } + + Process { + id: detectSystemctlProcess + running: false + command: ["sh", "-c", "command -v systemctl"] + + onExited: function (exitCode) { + systemctlCommandAvailable = (exitCode === 0); + } + } + Process { id: detectHibernateProcess running: false @@ -325,9 +349,14 @@ Singleton { } } + function powerManagerCommand(action) { + const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable); + return [useLoginctl ? "loginctl" : "systemctl", action]; + } + function suspend() { if (SettingsData.customPowerActionSuspend.length === 0) { - Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]); + Quickshell.execDetached(powerManagerCommand("suspend")); } else { Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]); } @@ -338,13 +367,13 @@ Singleton { if (SettingsData.customPowerActionHibernate.length > 0) { hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate]; } else { - hibernateProcess.command = [isElogind ? "loginctl" : "systemctl", "hibernate"]; + hibernateProcess.command = powerManagerCommand("hibernate"); } hibernateProcess.running = true; } function suspendThenHibernate() { - Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend-then-hibernate"]); + Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate")); } function suspendWithBehavior(behavior) { @@ -359,7 +388,7 @@ Singleton { function reboot() { if (SettingsData.customPowerActionReboot.length === 0) { - Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]); + Quickshell.execDetached(powerManagerCommand("reboot")); } else { Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]); } @@ -367,7 +396,7 @@ Singleton { function poweroff() { if (SettingsData.customPowerActionPowerOff.length === 0) { - Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"]); + Quickshell.execDetached(powerManagerCommand("poweroff")); } else { Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]); }