package distros import ( "context" "fmt" "os/exec" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" ) func init() { Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution { return NewNixOSDistribution(config, logChan) }) } type NixOSDistribution struct { *BaseDistribution config DistroConfig } func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution { base := NewBaseDistribution(logChan) return &NixOSDistribution{ BaseDistribution: base, config: config, } } func (n *NixOSDistribution) GetID() string { return n.config.ID } func (n *NixOSDistribution) GetColorHex() string { return n.config.ColorHex } func (n *NixOSDistribution) GetFamily() DistroFamily { return n.config.Family } func (n *NixOSDistribution) GetPackageManager() PackageManagerType { return PackageManagerNix } func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) } func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) { var dependencies []deps.Dependency // DMS at the top (shell is prominent) dependencies = append(dependencies, n.detectDMS()) // Terminal with choice support dependencies = append(dependencies, n.detectSpecificTerminal(terminal)) // Common detections using base methods dependencies = append(dependencies, n.detectGit()) dependencies = append(dependencies, n.detectWindowManager(wm)) dependencies = append(dependencies, n.detectQuickshell()) dependencies = append(dependencies, n.detectXDGPortal()) dependencies = append(dependencies, n.detectPolkitAgent()) dependencies = append(dependencies, n.detectAccountsService()) // Hyprland-specific tools if wm == deps.WindowManagerHyprland { dependencies = append(dependencies, n.detectHyprlandTools()...) } // Niri-specific tools if wm == deps.WindowManagerNiri { dependencies = append(dependencies, n.detectXwaylandSatellite()) } // Base detections (common across distros) dependencies = append(dependencies, n.detectMatugen()) dependencies = append(dependencies, n.detectDgop()) dependencies = append(dependencies, n.detectHyprpicker()) dependencies = append(dependencies, n.detectClipboardTools()...) return dependencies, nil } func (n *NixOSDistribution) detectDMS() deps.Dependency { status := deps.StatusMissing // For NixOS, check if quickshell can find the dms config cmd := exec.Command("qs", "-c", "dms", "--list") if err := cmd.Run(); err == nil { status = deps.StatusInstalled } else if n.packageInstalled("DankMaterialShell") { // Fallback: check if flake is in profile status = deps.StatusInstalled } return deps.Dependency{ Name: "dms (DankMaterialShell)", Status: status, Description: "Desktop Management System configuration (installed as flake)", Required: true, } } func (n *NixOSDistribution) detectXDGPortal() deps.Dependency { status := deps.StatusMissing if n.packageInstalled("xdg-desktop-portal-gtk") { status = deps.StatusInstalled } return deps.Dependency{ Name: "xdg-desktop-portal-gtk", Status: status, Description: "Desktop integration portal for GTK", Required: true, } } func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency { switch wm { case deps.WindowManagerHyprland: status := deps.StatusMissing description := "Dynamic tiling Wayland compositor" if n.commandExists("hyprland") || n.commandExists("Hyprland") { status = deps.StatusInstalled } else { description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix" } return deps.Dependency{ Name: "hyprland", Status: status, Description: description, Required: true, } case deps.WindowManagerNiri: status := deps.StatusMissing description := "Scrollable-tiling Wayland compositor" if n.commandExists("niri") { status = deps.StatusInstalled } else { description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix" } return deps.Dependency{ Name: "niri", Status: status, Description: description, Required: true, } default: return deps.Dependency{ Name: "unknown-wm", Status: deps.StatusMissing, Description: "Unknown window manager", Required: true, } } } func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency { var dependencies []deps.Dependency tools := []struct { name string description string }{ {"grim", "Screenshot utility for Wayland"}, {"slurp", "Region selection utility for Wayland"}, {"hyprctl", "Hyprland control utility (comes with system Hyprland)"}, {"hyprpicker", "Color picker for Hyprland"}, {"grimblast", "Screenshot script for Hyprland"}, {"jq", "JSON processor"}, } for _, tool := range tools { status := deps.StatusMissing // Special handling for hyprctl - it comes with system hyprland if tool.name == "hyprctl" { if n.commandExists("hyprctl") { status = deps.StatusInstalled } } else { if n.commandExists(tool.name) { status = deps.StatusInstalled } } dependencies = append(dependencies, deps.Dependency{ Name: tool.name, Status: status, Description: tool.description, Required: true, }) } return dependencies } func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency { status := deps.StatusMissing if n.commandExists("xwayland-satellite") { status = deps.StatusInstalled } return deps.Dependency{ Name: "xwayland-satellite", Status: status, Description: "Xwayland support", Required: true, } } func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency { status := deps.StatusMissing if n.packageInstalled("mate-polkit") { status = deps.StatusInstalled } return deps.Dependency{ Name: "mate-polkit", Status: status, Description: "PolicyKit authentication agent", Required: true, } } func (n *NixOSDistribution) detectAccountsService() deps.Dependency { status := deps.StatusMissing if n.packageInstalled("accountsservice") { status = deps.StatusInstalled } return deps.Dependency{ Name: "accountsservice", Status: status, Description: "D-Bus interface for user account query and manipulation", Required: true, } } func (n *NixOSDistribution) packageInstalled(pkg string) bool { cmd := exec.Command("nix", "profile", "list") output, err := cmd.Output() if err != nil { return false } return strings.Contains(string(output), pkg) } func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { packages := map[string]PackageMapping{ "git": {Name: "nixpkgs#git", Repository: RepoTypeSystem}, "quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake}, "matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake}, "dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake}, "dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake}, "ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem}, "alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem}, "cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem}, "accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem}, "hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem}, } // Note: Window managers (hyprland/niri) should be installed system-wide on NixOS // We only install the tools here switch wm { case deps.WindowManagerHyprland: // Skip hyprland itself - should be installed system-wide packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem} packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem} packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake} packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem} case deps.WindowManagerNiri: // Skip niri itself - should be installed system-wide packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake} } return packages } func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.10, Step: "NixOS prerequisites ready", IsComplete: false, LogOutput: "NixOS package manager is ready to use", } return nil } func (n *NixOSDistribution) 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 { // Phase 1: Check Prerequisites progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.05, Step: "Checking system prerequisites...", IsComplete: false, LogOutput: "Starting prerequisite check...", } if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install prerequisites: %w", err) } nixpkgsPkgs, flakePkgs, _ := n.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) // Phase 2: Nixpkgs Packages if len(nixpkgsPkgs) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.35, Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)), IsComplete: false, LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")), } if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil { return fmt.Errorf("failed to install nixpkgs packages: %w", err) } } // Phase 3: Flake Packages if len(flakePkgs) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseAURPackages, Progress: 0.65, Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)), IsComplete: false, LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")), } if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil { return fmt.Errorf("failed to install flake packages: %w", err) } } // Phase 4: Configuration progressChan <- InstallProgressMsg{ Phase: PhaseConfiguration, Progress: 0.90, Step: "Configuring system...", IsComplete: false, LogOutput: "Starting post-installation configuration...", } if err := n.postInstallConfig(progressChan); err != nil { return fmt.Errorf("failed to configure system: %w", err) } // Phase 5: Complete progressChan <- InstallProgressMsg{ Phase: PhaseComplete, Progress: 1.0, Step: "Installation complete!", IsComplete: true, LogOutput: "All packages installed and configured successfully", } return nil } func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) { nixpkgsPkgs := []string{} flakePkgs := []string{} variantMap := make(map[string]deps.PackageVariant) for _, dep := range dependencies { variantMap[dep.Name] = dep.Variant } packageMap := n.GetPackageMapping(wm) 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 { n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name)) continue } switch pkgInfo.Repository { case RepoTypeSystem: nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name) case RepoTypeFlake: flakePkgs = append(flakePkgs, pkgInfo.Name) } } return nixpkgsPkgs, flakePkgs, variantMap } func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error { if len(packages) == 0 { return nil } n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", "))) args := []string{"profile", "install"} args = append(args, packages...) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.40, Step: "Installing nixpkgs packages...", IsComplete: false, CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")), } cmd := exec.CommandContext(ctx, "nix", args...) return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) } func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error { if len(packages) == 0 { return nil } n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", "))) baseProgress := 0.65 progressStep := 0.20 / float64(len(packages)) for i, pkg := range packages { currentProgress := baseProgress + (float64(i) * progressStep) progressChan <- InstallProgressMsg{ Phase: PhaseAURPackages, Progress: currentProgress, Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)), IsComplete: false, CommandInfo: fmt.Sprintf("nix profile install %s", pkg), } cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg) if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil { return fmt.Errorf("failed to install flake package %s: %w", pkg, err) } } return nil } func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error { // For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone // The flake installation handles both the binary and config files correctly progressChan <- InstallProgressMsg{ Phase: PhaseConfiguration, Progress: 0.95, Step: "NixOS configuration complete", IsComplete: false, LogOutput: "DMS installed via flake - binary and config handled by Nix", } return nil }