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://void.danklinux.com/dms/current" VoidDankLinuxRepo = "https://void.danklinux.com/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()) dependencies = append(dependencies, v.detectMesaDri()) 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()) dependencies = append(dependencies, v.detectDanksearch()) dependencies = append(dependencies, v.detectDankCalendar()) 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) detectMesaDri() deps.Dependency { return v.detectPackage("mesa-dri", "Mesa DRI/EGL drivers (GPU rendering; compositors find no outputs without it)", v.packageInstalled("mesa-dri")) } 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}, "mesa-dri": {Name: "mesa-dri", 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}, "danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo}, "dankcalendar": {Name: "dankcalendar", 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 } // D-Bus activation alone starts elogind without its wrapper mounts; the runit service is required. 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 }