diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros')
14 files changed, 6386 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/arch.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/arch.go new file mode 100644 index 0000000..b412f88 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/arch.go @@ -0,0 +1,807 @@ +package distros + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +func init() { + Register("arch", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("archarm", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("archcraft", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("manjaro", "#35BF5C", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("obarun", "#2494be", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) + Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { + return NewArchDistribution(config, logChan) + }) +} + +type ArchDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig +} + +func NewArchDistribution(config DistroConfig, logChan chan<- string) *ArchDistribution { + base := NewBaseDistribution(logChan) + return &ArchDistribution{ + BaseDistribution: base, + ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, + config: config, + } +} + +func (a *ArchDistribution) GetID() string { + return a.config.ID +} + +func (a *ArchDistribution) GetColorHex() string { + return a.config.ColorHex +} + +func (a *ArchDistribution) GetFamily() DistroFamily { + return a.config.Family +} + +func (a *ArchDistribution) GetPackageManager() PackageManagerType { + return PackageManagerPacman +} + +func (a *ArchDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return a.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (a *ArchDistribution) 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, a.detectDMS()) + + // Terminal with choice support + dependencies = append(dependencies, a.detectSpecificTerminal(terminal)) + + // Common detections using base methods + dependencies = append(dependencies, a.detectGit()) + dependencies = append(dependencies, a.detectWindowManager(wm)) + dependencies = append(dependencies, a.detectQuickshell()) + dependencies = append(dependencies, a.detectDMSGreeter()) + dependencies = append(dependencies, a.detectXDGPortal()) + dependencies = append(dependencies, a.detectAccountsService()) + + // Hyprland-specific tools + if wm == deps.WindowManagerHyprland { + dependencies = append(dependencies, a.detectHyprlandTools()...) + } + + // Niri-specific tools + if wm == deps.WindowManagerNiri { + dependencies = append(dependencies, a.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, a.detectMatugen()) + dependencies = append(dependencies, a.detectDgop()) + + return dependencies, nil +} + +func (a *ArchDistribution) detectXDGPortal() deps.Dependency { + return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (a *ArchDistribution) detectAccountsService() deps.Dependency { + return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice")) +} + +func (a *ArchDistribution) detectDMSGreeter() deps.Dependency { + return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git")) +} + +func (a *ArchDistribution) packageInstalled(pkg string) bool { + cmd := exec.Command("pacman", "-Q", pkg) + err := cmd.Run() + return err == nil +} + +// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package +func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) { + data, err := os.ReadFile(srcinfoPath) + if err != nil { + return nil, nil, err + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + var pkg string + var target *[]string + switch { + case strings.HasPrefix(line, "makedepends = "): + pkg = strings.TrimPrefix(line, "makedepends = ") + target = &makedeps + case strings.HasPrefix(line, "depends = "): + pkg = strings.TrimPrefix(line, "depends = ") + target = &deps + default: + continue + } + // Strip version constraint (>=, <=, >, <, =) and colon-descriptions + if idx := strings.IndexAny(pkg, "><:="); idx >= 0 { + pkg = pkg[:idx] + } + pkg = strings.TrimSpace(pkg) + if pkg != "" { + *target = append(*target, pkg) + } + } + return deps, makedeps, nil +} + +func (a *ArchDistribution) isInSystemRepo(pkg string) bool { + return exec.Command("pacman", "-Si", pkg).Run() == nil +} + +func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + packages := map[string]PackageMapping{ + "dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]), + "git": {Name: "git", Repository: RepoTypeSystem}, + "quickshell": a.getQuickshellMapping(variants["quickshell"]), + "dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR}, + "matugen": a.getMatugenMapping(variants["matugen"]), + "dgop": {Name: "dgop", 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}, + } + + switch wm { + case deps.WindowManagerHyprland: + packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"]) + packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"]) + packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} + case deps.WindowManagerNiri: + packages["niri"] = a.getNiriMapping(variants["niri"]) + packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} + } + + return packages +} + +func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping { + if forceQuickshellGit || variant == deps.VariantGit { + return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} + } + // ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell + return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} +} + +func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { + return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} +} + +func (a *ArchDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "niri-git", Repository: RepoTypeAUR} + } + return PackageMapping{Name: "niri", Repository: RepoTypeSystem} +} + +func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping { + if runtime.GOARCH == "arm64" { + return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR} + } + + if variant == deps.VariantGit { + return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR} + } + return PackageMapping{Name: "matugen", Repository: RepoTypeSystem} +} + +func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping { + if forceDMSGit || variant == deps.VariantGit { + return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR} + } + + if a.packageInstalled("dms-shell-git") { + return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR} + } + + return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem} +} + +func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency { + status := deps.StatusMissing + if a.commandExists("xwayland-satellite") { + status = deps.StatusInstalled + } + + return deps.Dependency{ + Name: "xwayland-satellite", + Status: status, + Description: "Xwayland support", + Required: true, + } +} + +func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Checking base-devel...", + IsComplete: false, + LogOutput: "Checking if base-devel is installed", + } + + checkCmd := exec.CommandContext(ctx, "pacman", "-Qq", "base-devel") + if err := checkCmd.Run(); err == nil { + a.log("base-devel already installed") + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.10, + Step: "base-devel already installed", + IsComplete: false, + LogOutput: "base-devel is already installed on the system", + } + return nil + } + + a.log("Installing base-devel...") + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.08, + Step: "Installing base-devel...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo pacman -S --needed --noconfirm base-devel", + LogOutput: "Installing base-devel development tools", + } + + cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel") + if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil { + return fmt.Errorf("failed to install base-devel: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.12, + Step: "base-devel installation complete", + IsComplete: false, + LogOutput: "base-devel successfully installed", + } + + return nil +} + +func (a *ArchDistribution) 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 := a.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") { + if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to preinstall quickshell-git: %w", err) + } + aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" }) + } + + // Phase 3: System Packages + if len(systemPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), + } + if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install system packages: %w", err) + } + } + + // Phase 4: AUR Packages + if len(aurPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.65, + Step: fmt.Sprintf("Installing %d AUR packages...", len(aurPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Installing AUR packages: %s", strings.Join(aurPkgs, ", ")), + } + if err := a.installAURPackages(ctx, aurPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install AUR packages: %w", err) + } + } + + // Phase 5: Manual Builds + if len(manualPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.85, + Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), + } + if err := a.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install manual packages: %w", err) + } + } + + // Phase 6: Configuration + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + terminal := a.DetectTerminalFromDeps(dependencies) + if err := a.WriteEnvironmentConfig(terminal); err != nil { + a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := a.WriteWindowManagerConfig(wm); err != nil { + a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := a.EnableDMSService(ctx, wm); err != nil { + a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + // Phase 7: Complete + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, []string, map[string]deps.PackageVariant) { + systemPkgs := []string{} + aurPkgs := []string{} + manualPkgs := []string{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := a.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 { + manualPkgs = append(manualPkgs, dep.Name) + continue + } + + switch pkgInfo.Repository { + case RepoTypeAUR: + aurPkgs = append(aurPkgs, pkgInfo.Name) + case RepoTypeSystem: + systemPkgs = append(systemPkgs, pkgInfo.Name) + case RepoTypeManual: + manualPkgs = append(manualPkgs, dep.Name) + } + } + + return systemPkgs, aurPkgs, manualPkgs, variantMap +} + +func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if a.packageInstalled("quickshell-git") { + return nil + } + + if a.packageInstalled("quickshell") { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.15, + Step: "Removing stable quickshell...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo pacman -Rdd --noconfirm quickshell", + LogOutput: "Removing stable quickshell so quickshell-git can be installed", + } + cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell") + if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil { + return fmt.Errorf("failed to remove stable quickshell: %w", err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.18, + Step: "Building quickshell-git before system packages...", + IsComplete: false, + CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict", + } + return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32) +} + +func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", "))) + + args := []string{"pacman", "-S", "--needed", "--noconfirm"} + if slices.Contains(packages, "dms-shell") { + args = append(args, "--assume-installed", "dms-shell-compositor=1") + } + args = append(args, packages...) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.40, + Step: "Installing system packages...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) +} + +func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) + + hasNiri := false + for _, pkg := range packages { + if pkg == "niri-git" { + hasNiri = true + } + } + + // If niri is in the list, install makepkg-git-lfs-proto first if not already installed + if hasNiri { + if !a.packageInstalled("makepkg-git-lfs-proto") { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.65, + Step: "Installing makepkg-git-lfs-proto for niri...", + IsComplete: false, + CommandInfo: "Installing prerequisite for niri-git", + } + + if err := a.installSingleAURPackage(ctx, "makepkg-git-lfs-proto", sudoPassword, progressChan, 0.65, 0.67); err != nil { + return fmt.Errorf("failed to install makepkg-git-lfs-proto prerequisite for niri: %w", err) + } + } + } + + // Reorder packages to ensure dms-shell-git dependencies are installed first + orderedPackages := a.reorderAURPackages(packages) + + baseProgress := 0.67 + progressStep := 0.13 / float64(len(orderedPackages)) + + for i, pkg := range orderedPackages { + currentProgress := baseProgress + (float64(i) * progressStep) + + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: currentProgress, + Step: fmt.Sprintf("Installing AUR package %s (%d/%d)...", pkg, i+1, len(packages)), + IsComplete: false, + CommandInfo: fmt.Sprintf("Building and installing %s", pkg), + } + + if err := a.installSingleAURPackage(ctx, pkg, sudoPassword, progressChan, currentProgress, currentProgress+progressStep); err != nil { + return fmt.Errorf("failed to install AUR package %s: %w", pkg, err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.80, + Step: "All AUR packages installed successfully", + IsComplete: false, + LogOutput: fmt.Sprintf("Successfully installed AUR packages: %s", strings.Join(packages, ", ")), + } + + return nil +} + +func (a *ArchDistribution) reorderAURPackages(packages []string) []string { + dmsDepencies := []string{"quickshell", "quickshell-git", "dgop"} + + var deps []string + var others []string + var dmsShell []string + + for _, pkg := range packages { + if pkg == "dms-shell-git" { + dmsShell = append(dmsShell, pkg) + } else { + isDep := false + if slices.Contains(dmsDepencies, pkg) { + deps = append(deps, pkg) + isDep = true + } + if !isDep { + others = append(others, pkg) + } + } + } + + result := append(deps, others...) + result = append(result, dmsShell...) + return result +} + +func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error { + return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool)) +} + +func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error { + if visited[pkg] { + a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg)) + return nil + } + visited[pkg] = true + + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %w", err) + } + + buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "aur-builds", pkg) + + // Clean up any existing cache first + if err := os.RemoveAll(buildDir); err != nil { + a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err)) + } + + if err := os.MkdirAll(buildDir, 0o755); err != nil { + return fmt.Errorf("failed to create build directory: %w", err) + } + defer func() { + if removeErr := os.RemoveAll(buildDir); removeErr != nil { + a.log(fmt.Sprintf("Warning: failed to cleanup build directory %s: %v", buildDir, removeErr)) + } + }() + + // Clone the AUR package + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.1*(endProgress-startProgress), + Step: fmt.Sprintf("Cloning %s from AUR...", pkg), + IsComplete: false, + CommandInfo: fmt.Sprintf("git clone https://aur.archlinux.org/%s.git", pkg), + } + + cloneCmd := exec.CommandContext(ctx, "git", "clone", fmt.Sprintf("https://aur.archlinux.org/%s.git", pkg), filepath.Join(buildDir, pkg)) + if err := a.runWithProgress(cloneCmd, progressChan, PhaseAURPackages, startProgress+0.1*(endProgress-startProgress), startProgress+0.2*(endProgress-startProgress)); err != nil { + return fmt.Errorf("failed to clone %s: %w", pkg, err) + } + + packageDir := filepath.Join(buildDir, pkg) + + if pkg == "niri-git" { + pkgbuildPath := filepath.Join(packageDir, "PKGBUILD") + sedCmd := exec.CommandContext(ctx, "sed", "-i", "s/makepkg-git-lfs-proto//g", pkgbuildPath) + if err := sedCmd.Run(); err != nil { + return fmt.Errorf("failed to patch PKGBUILD for niri-git: %w", err) + } + + srcinfoPath := filepath.Join(packageDir, ".SRCINFO") + sedCmd2 := exec.CommandContext(ctx, "sed", "-i", "/makedepends = makepkg-git-lfs-proto/d", srcinfoPath) + if err := sedCmd2.Run(); err != nil { + return fmt.Errorf("failed to patch .SRCINFO for niri-git: %w", err) + } + } + + if pkg == "dms-shell-git" { + srcinfoPath := filepath.Join(packageDir, ".SRCINFO") + depsToRemove := []string{ + "depends = quickshell", + "depends = dgop", + } + + for _, dep := range depsToRemove { + sedCmd := exec.CommandContext(ctx, "sed", "-i", fmt.Sprintf("/%s/d", dep), srcinfoPath) + if err := sedCmd.Run(); err != nil { + return fmt.Errorf("failed to remove dependency %s from .SRCINFO for %s: %w", dep, pkg, err) + } + } + } + + // Remove all optdepends from .SRCINFO for all packages + srcinfoPath := filepath.Join(packageDir, ".SRCINFO") + optdepsCmd := exec.CommandContext(ctx, "sed", "-i", "/^[[:space:]]*optdepends = /d", srcinfoPath) + if err := optdepsCmd.Run(); err != nil { + return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) + } + + srcinfoPath = filepath.Join(packageDir, ".SRCINFO") + { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.3*(endProgress-startProgress), + Step: fmt.Sprintf("Resolving dependencies for %s...", pkg), + IsComplete: false, + CommandInfo: "Classifying dependencies as system or AUR", + } + + runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath) + if err != nil { + return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err) + } + + seen := make(map[string]bool) + var systemPkgs []string + var aurPkgs []string + + for _, dep := range append(runtimeDeps, makeDeps...) { + if seen[dep] || a.packageInstalled(dep) { + continue + } + seen[dep] = true + if a.isInSystemRepo(dep) { + systemPkgs = append(systemPkgs, dep) + } else { + aurPkgs = append(aurPkgs, dep) + } + } + + if len(systemPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.32*(endProgress-startProgress), + Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg), + IsComplete: false, + CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")), + } + if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err) + } + } + + for _, aurDep := range aurPkgs { + a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep)) + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.35*(endProgress-startProgress), + Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg), + IsComplete: false, + CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep), + } + if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan, + startProgress+0.35*(endProgress-startProgress), + startProgress+0.39*(endProgress-startProgress), + visited, + ); err != nil { + return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err) + } + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.4*(endProgress-startProgress), + Step: fmt.Sprintf("Building %s...", pkg), + IsComplete: false, + CommandInfo: "makepkg --noconfirm", + } + + buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") + buildCmd.Dir = packageDir + buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") + + if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { + return fmt.Errorf("failed to build %s: %w", pkg, err) + } + + // Find built package file + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.7*(endProgress-startProgress), + Step: fmt.Sprintf("Installing %s...", pkg), + IsComplete: false, + CommandInfo: "sudo pacman -U built-package", + } + + var files []string + matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*")) + files = matches + + if len(files) == 0 { + return fmt.Errorf("no package files found after building %s", pkg) + } + + installArgs := []string{"pacman", "-U", "--noconfirm"} + installArgs = append(installArgs, files...) + + installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " ")) + + fileNames := make([]string, len(files)) + for i, f := range files { + fileNames[i] = filepath.Base(f) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress + 0.7*(endProgress-startProgress), + LogOutput: fmt.Sprintf("Installing packages: %s", strings.Join(fileNames, ", ")), + } + + if err := a.runWithProgress(installCmd, progressChan, PhaseAURPackages, startProgress+0.7*(endProgress-startProgress), endProgress); err != nil { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: startProgress, + LogOutput: fmt.Sprintf("ERROR: pacman -U failed for %s with error: %v", pkg, err), + Error: err, + } + return fmt.Errorf("failed to install built package %s: %w", pkg, err) + } + + a.log(fmt.Sprintf("Successfully installed AUR package: %s", pkg)) + return nil +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base.go new file mode 100644 index 0000000..63555c8 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base.go @@ -0,0 +1,721 @@ +package distros + +import ( + "bufio" + "context" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "github.com/AvengeMedia/DankMaterialShell/core/internal/version" +) + +const ( + forceQuickshellGit = false + forceDMSGit = false +) + +// BaseDistribution provides common functionality for all distributions +type BaseDistribution struct { + logChan chan<- string +} + +// NewBaseDistribution creates a new base distribution +func NewBaseDistribution(logChan chan<- string) *BaseDistribution { + return &BaseDistribution{ + logChan: logChan, + } +} + +// Common helper methods +func (b *BaseDistribution) commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +func (b *BaseDistribution) CommandExists(cmd string) bool { + return b.commandExists(cmd) +} + +func (b *BaseDistribution) log(message string) { + if b.logChan != nil { + b.logChan <- message + } +} + +func (b *BaseDistribution) logError(message string, err error) { + errorMsg := fmt.Sprintf("ERROR: %s: %v", message, err) + b.log(errorMsg) +} + +// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings. +// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote. +// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes. +func escapeSingleQuotes(s string) string { + return strings.ReplaceAll(s, "'", "'\\''") +} + +// MakeSudoCommand creates a command string that safely passes password to sudo. +// This helper escapes special characters in the password to prevent shell injection +// and syntax errors when passwords contain single quotes, apostrophes, or other special chars. +func MakeSudoCommand(sudoPassword string, command string) string { + return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command) +} + +// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password. +// The password is properly escaped to prevent shell injection and syntax errors. +func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd { + cmdStr := MakeSudoCommand(sudoPassword, command) + return exec.CommandContext(ctx, "bash", "-c", cmdStr) +} + +func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency { + status := deps.StatusMissing + if b.commandExists(name) { + status = deps.StatusInstalled + } + return deps.Dependency{ + Name: name, + Status: status, + Description: description, + Required: true, + } +} + +func (b *BaseDistribution) detectPackage(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: true, + } +} + +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") +} + +func (b *BaseDistribution) detectMatugen() deps.Dependency { + return b.detectCommand("matugen", "Material Design color generation tool") +} + +func (b *BaseDistribution) detectDgop() deps.Dependency { + return b.detectCommand("dgop", "Desktop portal management tool") +} + +func (b *BaseDistribution) detectDMS() deps.Dependency { + dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms") + + status := deps.StatusMissing + currentVersion := "" + + if _, err := os.Stat(dmsPath); err == nil { + status = deps.StatusInstalled + + // Only get current version, don't check for updates (lazy loading) + current, err := version.GetCurrentDMSVersion() + if err == nil { + currentVersion = current + } + } + + dep := deps.Dependency{ + Name: "dms (DankMaterialShell)", + Status: status, + Description: "Desktop Management System configuration", + Required: true, + CanToggle: true, + } + + if currentVersion != "" { + dep.Version = currentVersion + } + + return dep +} + +func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.Dependency { + switch terminal { + case deps.TerminalGhostty: + status := deps.StatusMissing + if b.commandExists("ghostty") { + status = deps.StatusInstalled + } + return deps.Dependency{ + Name: "ghostty", + Status: status, + Description: "A fast, native terminal emulator built in Zig.", + Required: true, + } + case deps.TerminalKitty: + status := deps.StatusMissing + if b.commandExists("kitty") { + status = deps.StatusInstalled + } + return deps.Dependency{ + Name: "kitty", + Status: status, + Description: "A feature-rich, customizable terminal emulator.", + Required: true, + } + case deps.TerminalAlacritty: + status := deps.StatusMissing + if b.commandExists("alacritty") { + status = deps.StatusInstalled + } + return deps.Dependency{ + Name: "alacritty", + Status: status, + Description: "A simple terminal emulator. (No dynamic theming)", + Required: true, + } + default: + return b.detectSpecificTerminal(deps.TerminalGhostty) + } +} + +func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency { + var dependencies []deps.Dependency + + tools := []struct { + name string + description string + }{ + {"hyprctl", "Hyprland control utility"}, + {"jq", "JSON processor"}, + } + + for _, tool := range tools { + status := deps.StatusMissing + if b.commandExists(tool.name) { + status = deps.StatusInstalled + } + + dependencies = append(dependencies, deps.Dependency{ + Name: tool.name, + Status: status, + Description: tool.description, + Required: true, + }) + } + + return dependencies +} + +func (b *BaseDistribution) detectQuickshell() deps.Dependency { + if !b.commandExists("qs") { + return deps.Dependency{ + Name: "quickshell", + Status: deps.StatusMissing, + Description: "QtQuick based desktop shell toolkit", + Required: true, + Variant: deps.VariantStable, + CanToggle: true, + } + } + + cmd := exec.Command("qs", "--version") + output, err := cmd.Output() + if err != nil { + return deps.Dependency{ + Name: "quickshell", + Status: deps.StatusNeedsReinstall, + Description: "QtQuick based desktop shell toolkit (version check failed)", + Required: true, + Variant: deps.VariantStable, + CanToggle: true, + } + } + + versionStr := string(output) + versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`) + matches := versionRegex.FindStringSubmatch(versionStr) + + if len(matches) < 2 { + return deps.Dependency{ + Name: "quickshell", + Status: deps.StatusNeedsReinstall, + Description: "QtQuick based desktop shell toolkit (unknown version)", + Required: true, + Variant: deps.VariantStable, + CanToggle: true, + } + } + + version := matches[1] + variant := deps.VariantStable + if strings.Contains(versionStr, "git") || strings.Contains(versionStr, "+") { + variant = deps.VariantGit + } + + if b.versionCompare(version, "0.2.0") >= 0 { + return deps.Dependency{ + Name: "quickshell", + Status: deps.StatusInstalled, + Version: version, + Description: "QtQuick based desktop shell toolkit", + Required: true, + Variant: variant, + CanToggle: true, + } + } + + return deps.Dependency{ + Name: "quickshell", + Status: deps.StatusNeedsUpdate, + Variant: variant, + CanToggle: true, + Version: version, + Description: "QtQuick based desktop shell toolkit (needs 0.2.0+)", + Required: true, + } +} + +func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency { + switch wm { + case deps.WindowManagerHyprland: + status := deps.StatusMissing + variant := deps.VariantStable + version := "" + + if b.commandExists("hyprland") || b.commandExists("Hyprland") { + status = deps.StatusInstalled + cmd := exec.Command("hyprctl", "version") + if output, err := cmd.Output(); err == nil { + outStr := string(output) + if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") { + variant = deps.VariantGit + } + if versionRegex := regexp.MustCompile(`v(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) { + matches := versionRegex.FindStringSubmatch(outStr) + if len(matches) > 1 { + version = matches[1] + } + } + } + } + return deps.Dependency{ + Name: "hyprland", + Status: status, + Version: version, + Description: "Dynamic tiling Wayland compositor", + Required: true, + Variant: variant, + CanToggle: true, + } + case deps.WindowManagerNiri: + status := deps.StatusMissing + variant := deps.VariantStable + version := "" + + if b.commandExists("niri") { + status = deps.StatusInstalled + cmd := exec.Command("niri", "--version") + if output, err := cmd.Output(); err == nil { + outStr := string(output) + if strings.Contains(outStr, "git") || strings.Contains(outStr, "+") { + variant = deps.VariantGit + } + if versionRegex := regexp.MustCompile(`niri (\d+\.\d+)`); versionRegex.MatchString(outStr) { + matches := versionRegex.FindStringSubmatch(outStr) + if len(matches) > 1 { + version = matches[1] + } + } + } + } + return deps.Dependency{ + Name: "niri", + Status: status, + Version: version, + Description: "Scrollable-tiling Wayland compositor", + Required: true, + Variant: variant, + CanToggle: true, + } + default: + return deps.Dependency{ + Name: "unknown-wm", + Status: deps.StatusMissing, + Description: "Unknown window manager", + Required: true, + } + } +} + +// Version comparison helper +func (b *BaseDistribution) versionCompare(v1, v2 string) int { + parts1 := strings.Split(v1, ".") + parts2 := strings.Split(v2, ".") + + for i := 0; i < len(parts1) && i < len(parts2); i++ { + if parts1[i] < parts2[i] { + return -1 + } + if parts1[i] > parts2[i] { + return 1 + } + } + + if len(parts1) < len(parts2) { + return -1 + } + if len(parts1) > len(parts2) { + return 1 + } + + return 0 +} + +// Common installation helper +func (b *BaseDistribution) runWithProgress(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64) error { + return b.runWithProgressTimeout(cmd, progressChan, phase, startProgress, endProgress, 20*time.Minute) +} + +func (b *BaseDistribution) runWithProgressTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, timeout time.Duration) error { + return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, "Installing...", timeout) +} + +func (b *BaseDistribution) runWithProgressStep(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string) error { + return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, stepMessage, 20*time.Minute) +} + +func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string, timeoutDuration time.Duration) error { + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %w", err) + } + stderr, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("failed to create stderr pipe: %w", err) + } + + if err := cmd.Start(); err != nil { + return err + } + + outputChan := make(chan string, 100) + done := make(chan error, 1) + + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + b.log(line) + outputChan <- line + } + }() + + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + line := scanner.Text() + b.log(line) + outputChan <- line + } + }() + + go func() { + done <- cmd.Wait() + close(outputChan) + }() + + ticker := time.NewTicker(200 * time.Millisecond) + defer ticker.Stop() + + progress := startProgress + progressStep := (endProgress - startProgress) / 50 + lastOutput := "" + + var timeout *time.Timer + var timeoutChan <-chan time.Time + if timeoutDuration > 0 { + timeout = time.NewTimer(timeoutDuration) + defer timeout.Stop() + timeoutChan = timeout.C + } + + for { + select { + case err := <-done: + if err != nil { + b.logError("Command execution failed", err) + b.log(fmt.Sprintf("Last output before failure: %s", lastOutput)) + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: startProgress, + Step: "Command failed", + IsComplete: false, + LogOutput: lastOutput, + Error: err, + } + return err + } + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: endProgress, + Step: "Installation step complete", + IsComplete: false, + LogOutput: lastOutput, + } + return nil + case output, ok := <-outputChan: + if ok { + lastOutput = output + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: progress, + Step: stepMessage, + IsComplete: false, + LogOutput: output, + } + if timeout != nil { + timeout.Reset(timeoutDuration) + } + } + case <-timeoutChan: + if cmd.Process != nil { + cmd.Process.Kill() + } + err := fmt.Errorf("installation timed out after %v", timeoutDuration) + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: startProgress, + Step: "Installation timed out", + IsComplete: false, + LogOutput: lastOutput, + Error: err, + } + return err + case <-ticker.C: + if progress < endProgress-0.01 { + progress += progressStep + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: progress, + Step: "Installing...", + IsComplete: false, + LogOutput: lastOutput, + } + } + } + } +} + +func (b *BaseDistribution) DetectTerminalFromDeps(dependencies []deps.Dependency) deps.Terminal { + for _, dep := range dependencies { + switch dep.Name { + case "ghostty": + return deps.TerminalGhostty + case "kitty": + return deps.TerminalKitty + case "alacritty": + return deps.TerminalAlacritty + } + } + return deps.TerminalGhostty +} + +func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + + envDir := filepath.Join(homeDir, ".config", "environment.d") + if err := os.MkdirAll(envDir, 0o755); err != nil { + return fmt.Errorf("failed to create environment.d directory: %w", err) + } + + var terminalCmd string + switch terminal { + case deps.TerminalGhostty: + terminalCmd = "ghostty" + case deps.TerminalKitty: + terminalCmd = "kitty" + case deps.TerminalAlacritty: + terminalCmd = "alacritty" + default: + terminalCmd = "ghostty" + } + + content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto +TERMINAL=%s +`, terminalCmd) + + envFile := filepath.Join(envDir, "90-dms.conf") + if err := os.WriteFile(envFile, []byte(content), 0o644); err != nil { + return fmt.Errorf("failed to write environment config: %w", err) + } + + b.log(fmt.Sprintf("Wrote environment config to %s", envFile)) + return nil +} + +func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error { + switch wm { + case deps.WindowManagerNiri: + if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil { + b.log("Warning: failed to add dms as a want for niri.service") + } + case deps.WindowManagerHyprland: + if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil { + b.log("Warning: failed to add dms as a want for hyprland-session.target") + } + } + + return nil +} + +func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error { + if wm == deps.WindowManagerHyprland { + if err := b.WriteHyprlandSessionTarget(); err != nil { + return fmt.Errorf("failed to write hyprland session target: %w", err) + } + } + return nil +} + +func (b *BaseDistribution) WriteHyprlandSessionTarget() error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + + targetDir := filepath.Join(homeDir, ".config", "systemd", "user") + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return fmt.Errorf("failed to create systemd user directory: %w", err) + } + + targetPath := filepath.Join(targetDir, "hyprland-session.target") + content := `[Unit] +Description=Hyprland Session Target +Requires=graphical-session.target +After=graphical-session.target +` + + if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil { + return fmt.Errorf("failed to write hyprland-session.target: %w", err) + } + + b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath)) + return nil +} + +// installDMSBinary installs the DMS binary from GitHub releases +func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + b.log("Installing/updating DMS binary...") + + // Detect architecture + arch := runtime.GOARCH + switch arch { + case "amd64": + case "arm64": + default: + return fmt.Errorf("unsupported architecture for DMS: %s", arch) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.80, + Step: "Downloading DMS binary...", + IsComplete: false, + CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch), + } + + // Get latest release version + latestVersionCmd := exec.CommandContext(ctx, "bash", "-c", + `curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`) + versionOutput, err := latestVersionCmd.Output() + if err != nil { + return fmt.Errorf("failed to get latest DMS version: %w", err) + } + version := strings.TrimSpace(string(versionOutput)) + if version == "" { + return fmt.Errorf("could not determine latest DMS version") + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %w", err) + } + tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + // Download the gzipped binary + downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch) + gzPath := filepath.Join(tmpDir, "dms.gz") + + downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath) + if err := downloadCmd.Run(); err != nil { + return fmt.Errorf("failed to download DMS binary: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.85, + Step: "Extracting DMS binary...", + IsComplete: false, + CommandInfo: "gunzip dms.gz", + } + + // Extract the binary + extractCmd := exec.CommandContext(ctx, "gunzip", gzPath) + if err := extractCmd.Run(); err != nil { + return fmt.Errorf("failed to extract DMS binary: %w", err) + } + + binaryPath := filepath.Join(tmpDir, "dms") + + // Make it executable + chmodCmd := exec.CommandContext(ctx, "chmod", "+x", binaryPath) + if err := chmodCmd.Run(); err != nil { + return fmt.Errorf("failed to make DMS binary executable: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.88, + Step: "Installing DMS to /usr/local/bin...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo cp dms /usr/local/bin/", + } + + // Install to /usr/local/bin + installCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath)) + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install DMS binary: %w", err) + } + + b.log("DMS binary installed successfully") + return nil +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base_test.go new file mode 100644 index 0000000..51e86d7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base_test.go @@ -0,0 +1,216 @@ +package distros + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" +) + +func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) { + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + + tempDir := t.TempDir() + os.Setenv("HOME", tempDir) + + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + dep := base.detectDMS() + + if dep.Status != deps.StatusMissing { + t.Errorf("Expected StatusMissing, got %d", dep.Status) + } + + if dep.Name != "dms (DankMaterialShell)" { + t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name) + } + + if !dep.Required { + t.Error("Expected Required to be true") + } +} + +func TestBaseDistribution_detectDMS_Installed(t *testing.T) { + if !utils.CommandExists("git") { + t.Skip("git not available") + } + + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + exec.Command("git", "init", dmsPath).Run() + exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run() + exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run() + + testFile := filepath.Join(dmsPath, "test.txt") + os.WriteFile(testFile, []byte("test"), 0o644) + exec.Command("git", "-C", dmsPath, "add", ".").Run() + exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run() + + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + dep := base.detectDMS() + + if dep.Status == deps.StatusMissing { + t.Error("Expected DMS to be detected as installed") + } + + if dep.Name != "dms (DankMaterialShell)" { + t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name) + } + + if !dep.Required { + t.Error("Expected Required to be true") + } + + t.Logf("Status: %d, Version: %s", dep.Status, dep.Version) +} + +func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) { + if !utils.CommandExists("git") { + t.Skip("git not available") + } + + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + exec.Command("git", "init", dmsPath).Run() + exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run() + exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run() + + testFile := filepath.Join(dmsPath, "test.txt") + os.WriteFile(testFile, []byte("test"), 0o644) + exec.Command("git", "-C", dmsPath, "add", ".").Run() + exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run() + exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run() + exec.Command("git", "-C", dmsPath, "checkout", "v0.0.1").Run() + + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + dep := base.detectDMS() + + if dep.Name != "dms (DankMaterialShell)" { + t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name) + } + + if !dep.Required { + t.Error("Expected Required to be true") + } + + t.Logf("Status: %d, Version: %s", dep.Status, dep.Version) +} + +func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) { + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + dep := base.detectDMS() + + if dep.Status == deps.StatusMissing { + t.Error("Expected DMS to be detected as present") + } + + if dep.Name != "dms (DankMaterialShell)" { + t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name) + } + + if !dep.Required { + t.Error("Expected Required to be true") + } +} + +func TestBaseDistribution_NewBaseDistribution(t *testing.T) { + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + + if base == nil { + t.Fatal("NewBaseDistribution returned nil") + } + + if base.logChan == nil { + t.Error("logChan was not set") + } +} + +func TestBaseDistribution_versionCompare(t *testing.T) { + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + + tests := []struct { + v1 string + v2 string + expected int + }{ + {"0.1.0", "0.1.0", 0}, + {"0.1.0", "0.1.1", -1}, + {"0.1.1", "0.1.0", 1}, + {"0.2.0", "0.1.9", 1}, + {"1.0.0", "0.9.9", 1}, + } + + for _, tt := range tests { + result := base.versionCompare(tt.v1, tt.v2) + if result != tt.expected { + t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected) + } + } +} + +func TestBaseDistribution_versionCompare_WithPrefix(t *testing.T) { + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + + tests := []struct { + v1 string + v2 string + expected int + }{ + {"v0.1.0", "v0.1.0", 0}, + {"v0.1.0", "v0.1.1", -1}, + {"v0.1.1", "v0.1.0", 1}, + } + + for _, tt := range tests { + result := base.versionCompare(tt.v1, tt.v2) + if result != tt.expected { + t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected) + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/debian.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/debian.go new file mode 100644 index 0000000..a3537ff --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/debian.go @@ -0,0 +1,706 @@ +package distros + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +func init() { + Register("debian", "#A80030", FamilyDebian, func(config DistroConfig, logChan chan<- string) Distribution { + return NewDebianDistribution(config, logChan) + }) +} + +type DebianDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig +} + +func NewDebianDistribution(config DistroConfig, logChan chan<- string) *DebianDistribution { + base := NewBaseDistribution(logChan) + return &DebianDistribution{ + BaseDistribution: base, + ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, + config: config, + } +} + +func (d *DebianDistribution) GetID() string { + return d.config.ID +} + +func (d *DebianDistribution) GetColorHex() string { + return d.config.ColorHex +} + +func (d *DebianDistribution) GetFamily() DistroFamily { + return d.config.Family +} + +func (d *DebianDistribution) GetPackageManager() PackageManagerType { + return PackageManagerAPT +} + +func (d *DebianDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return d.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) { + var dependencies []deps.Dependency + + dependencies = append(dependencies, d.detectDMS()) + + dependencies = append(dependencies, d.detectSpecificTerminal(terminal)) + + dependencies = append(dependencies, d.detectGit()) + dependencies = append(dependencies, d.detectWindowManager(wm)) + dependencies = append(dependencies, d.detectQuickshell()) + dependencies = append(dependencies, d.detectDMSGreeter()) + dependencies = append(dependencies, d.detectXDGPortal()) + dependencies = append(dependencies, d.detectAccountsService()) + + if wm == deps.WindowManagerNiri { + dependencies = append(dependencies, d.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, d.detectMatugen()) + dependencies = append(dependencies, d.detectDgop()) + + return dependencies, nil +} + +func (d *DebianDistribution) detectXDGPortal() deps.Dependency { + return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency { + return d.detectCommand("xwayland-satellite", "Xwayland support") +} + +func (d *DebianDistribution) detectAccountsService() deps.Dependency { + return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice")) +} + +func (d *DebianDistribution) detectDMSGreeter() deps.Dependency { + return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter")) +} + +func (d *DebianDistribution) packageInstalled(pkg string) bool { + return debianPackageInstalledPrecisely(pkg) +} + +func debianPackageInstalledPrecisely(pkg string) bool { + cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg) + output, err := cmd.Output() + if err != nil { + return false + } + return strings.TrimSpace(string(output)) == "installed" +} + +func debianRepoArchitecture(arch string) string { + switch arch { + case "amd64", "x86_64": + return "amd64" + case "arm64", "aarch64": + return "arm64" + default: + return arch + } +} + +func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + packages := map[string]PackageMapping{ + // Standard APT packages + "git": {Name: "git", 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}, + + // DMS packages from OBS with variant support + "dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": d.getQuickshellMapping(variants["quickshell"]), + "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + } + + if wm == deps.WindowManagerNiri { + niriVariant := variants["niri"] + packages["niri"] = d.getNiriMapping(niriVariant) + packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant) + } + + return packages +} + +func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"} + } + return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"} +} + +func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping { + if forceQuickshellGit || variant == deps.VariantGit { + return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} + } + return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} +} + +func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} + } + return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} +} + +func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} + } + return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} +} + +func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Updating package lists...", + IsComplete: false, + LogOutput: "Updating APT package lists", + } + + updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") + if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil { + return fmt.Errorf("failed to update package lists: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.08, + Step: "Installing build-essential...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install -y build-essential", + LogOutput: "Installing build tools", + } + + checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") + if err := checkCmd.Run(); err != nil { + cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential") + if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { + return fmt.Errorf("failed to install build-essential: %w", err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.10, + Step: "Installing development dependencies...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", + LogOutput: "Installing additional development tools", + } + + devToolsCmd := ExecSudoCommand(ctx, sudoPassword, + "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") + if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { + return fmt.Errorf("failed to install development tools: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.12, + Step: "Prerequisites installation complete", + IsComplete: false, + LogOutput: "Prerequisites successfully installed", + } + + return nil +} + +func (d *DebianDistribution) 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 err := d.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + systemPkgs, obsPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + // Enable OBS repositories + if len(obsPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.15, + Step: "Enabling OBS repositories...", + IsComplete: false, + LogOutput: "Setting up OBS repositories for additional packages", + } + if err := d.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to enable OBS repositories: %w", err) + } + } + + // System Packages + if len(systemPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), + } + if err := d.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install APT packages: %w", err) + } + } + + // OBS Packages + obsPkgNames := d.extractPackageNames(obsPkgs) + if len(obsPkgNames) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.65, + Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)), + IsComplete: false, + LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), + } + if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install OBS packages: %w", err) + } + } + + // Manual Builds + if len(manualPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.80, + Step: "Installing build dependencies...", + IsComplete: false, + LogOutput: "Installing build tools for manual compilation", + } + if err := d.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install build dependencies: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.85, + Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), + } + if err := d.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install manual packages: %w", err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + terminal := d.DetectTerminalFromDeps(dependencies) + if err := d.WriteEnvironmentConfig(terminal); err != nil { + d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := d.WriteWindowManagerConfig(wm); err != nil { + d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := d.EnableDMSService(ctx, wm); err != nil { + d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { + systemPkgs := []string{} + obsPkgs := []PackageMapping{} + manualPkgs := []string{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := d.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 { + d.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) + continue + } + + switch pkgInfo.Repository { + case RepoTypeSystem: + systemPkgs = append(systemPkgs, pkgInfo.Name) + case RepoTypeOBS: + obsPkgs = append(obsPkgs, pkgInfo) + case RepoTypeManual: + manualPkgs = append(manualPkgs, dep.Name) + } + } + + return systemPkgs, obsPkgs, manualPkgs, variantMap +} + +func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string { + names := make([]string, len(packages)) + for i, pkg := range packages { + names[i] = pkg.Name + } + return names +} + +func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string { + args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} + if minimal { + args = append(args, "--no-install-recommends") + } + return append(args, packages...) +} + +func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + enabledRepos := make(map[string]bool) + + osInfo, err := GetOSInfo() + if err != nil { + return fmt.Errorf("failed to get OS info: %w", err) + } + + // Determine Debian version for OBS repository URL + debianVersion := "Debian_13" + if osInfo.VersionID == "testing" { + debianVersion = "Debian_Testing" + } else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") { + debianVersion = "Debian_Unstable" + } + + for _, pkg := range obsPkgs { + if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] { + d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL)) + + // RepoURL format: "home:AvengeMedia:danklinux" + repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/") + repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-") + baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion) + + // Check if repository already exists + listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName) + checkCmd := exec.CommandContext(ctx, "test", "-f", listFile) + if checkCmd.Run() == nil { + d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL)) + enabledRepos[pkg.RepoURL] = true + continue + } + + keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName) + + // Create keyrings directory if it doesn't exist + mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings") + if err := mkdirCmd.Run(); err != nil { + d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err)) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.18, + Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL), + NeedsSudo: true, + CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL), + } + + keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath) + cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd) + if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil { + return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err) + } + + // Add repository + repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.20, + Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL), + NeedsSudo: true, + CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile), + } + + addRepoCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile)) + if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { + return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err) + } + + enabledRepos[pkg.RepoURL] = true + d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL)) + } + } + + if len(enabledRepos) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.25, + Step: "Updating package lists...", + NeedsSudo: true, + CommandInfo: "sudo apt-get update", + } + + updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") + if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { + return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err) + } + } + + return nil +} + +func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) + + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) + + groupIndex := 0 + installGroup := func(groupPackages []string, minimal bool) error { + if len(groupPackages) == 0 { + return nil + } + + groupIndex++ + startProgress := 0.40 + endProgress := 0.60 + if totalGroups > 1 { + if groupIndex == 1 { + endProgress = 0.50 + } else { + startProgress = 0.50 + } + } + + args := d.aptInstallArgs(groupPackages, minimal) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: startProgress, + Step: "Installing system packages...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress) + } + + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil +} + +func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + buildDeps := make(map[string]bool) + + for _, pkg := range manualPkgs { + switch pkg { + case "niri": + buildDeps["curl"] = true + buildDeps["libxkbcommon-dev"] = true + buildDeps["libwayland-dev"] = true + buildDeps["libudev-dev"] = true + buildDeps["libinput-dev"] = true + buildDeps["libdisplay-info-dev"] = true + buildDeps["libpango1.0-dev"] = true + buildDeps["libcairo-dev"] = true + buildDeps["libpipewire-0.3-dev"] = true + buildDeps["libc6-dev"] = true + buildDeps["clang"] = true + buildDeps["libseat-dev"] = true + buildDeps["libgbm-dev"] = true + buildDeps["alacritty"] = true + buildDeps["fuzzel"] = true + case "quickshell": + buildDeps["qt6-base-dev"] = true + buildDeps["qt6-base-private-dev"] = true + buildDeps["qt6-declarative-dev"] = true + buildDeps["qt6-declarative-private-dev"] = true + buildDeps["qt6-wayland-dev"] = true + buildDeps["qt6-wayland-private-dev"] = true + buildDeps["qt6-tools-dev"] = true + buildDeps["libqt6svg6-dev"] = true + buildDeps["qt6-shadertools-dev"] = true + buildDeps["spirv-tools"] = true + buildDeps["libcli11-dev"] = true + buildDeps["libjemalloc-dev"] = true + buildDeps["libwayland-dev"] = true + buildDeps["wayland-protocols"] = true + buildDeps["libdrm-dev"] = true + buildDeps["libgbm-dev"] = true + buildDeps["libegl-dev"] = true + buildDeps["libgles2-mesa-dev"] = true + buildDeps["libgl1-mesa-dev"] = true + buildDeps["libxcb1-dev"] = true + buildDeps["libpipewire-0.3-dev"] = true + buildDeps["libpam0g-dev"] = true + case "ghostty": + buildDeps["curl"] = true + case "matugen": + buildDeps["curl"] = true + } + } + + for _, pkg := range manualPkgs { + switch pkg { + case "niri", "matugen": + if err := d.installRust(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install Rust: %w", err) + } + case "dgop": + if err := d.installGo(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install Go: %w", err) + } + } + } + + if len(buildDeps) == 0 { + return nil + } + + depList := make([]string, 0, len(buildDeps)) + for dep := range buildDeps { + depList = append(depList, dep) + } + + args := []string{"apt-get", "install", "-y"} + args = append(args, depList...) + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82) +} + +func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if d.commandExists("cargo") { + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.82, + Step: "Installing rustup...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install rustup", + } + + rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup") + if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { + return fmt.Errorf("failed to install rustup: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.83, + Step: "Installing stable Rust toolchain...", + IsComplete: false, + CommandInfo: "rustup install stable", + } + + rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable") + if err := d.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil { + return fmt.Errorf("failed to install Rust toolchain: %w", err) + } + + if !d.commandExists("cargo") { + d.log("Warning: cargo not found in PATH after Rust installation, trying to source environment") + } + + return nil +} + +func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if d.commandExists("go") { + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.87, + Step: "Installing Go...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install golang-go", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go") + return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) +} + +func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + d.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", "))) + + for _, pkg := range packages { + switch pkg { + default: + if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install %s: %w", pkg, err) + } + } + } + + return nil +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/factory.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/factory.go new file mode 100644 index 0000000..9c3826a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/factory.go @@ -0,0 +1,19 @@ +package distros + +import ( + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +// NewDependencyDetector creates a DependencyDetector for the specified distribution +func NewDependencyDetector(distribution string, logChan chan<- string) (deps.DependencyDetector, error) { + distro, err := NewDistribution(distribution, logChan) + if err != nil { + return nil, err + } + return distro, nil +} + +// NewPackageInstaller creates a Distribution for package installation +func NewPackageInstaller(distribution string, logChan chan<- string) (Distribution, error) { + return NewDistribution(distribution, logChan) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/fedora.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/fedora.go new file mode 100644 index 0000000..4c16d4e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/fedora.go @@ -0,0 +1,550 @@ +package distros + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +func init() { + Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { + return NewFedoraDistribution(config, logChan) + }) + Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { + return NewFedoraDistribution(config, logChan) + }) + Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { + return NewFedoraDistribution(config, logChan) + }) + Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { + return NewFedoraDistribution(config, logChan) + }) + Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { + return NewFedoraDistribution(config, logChan) + }) + Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { + return NewFedoraDistribution(config, logChan) + }) +} + +type FedoraDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig +} + +func NewFedoraDistribution(config DistroConfig, logChan chan<- string) *FedoraDistribution { + base := NewBaseDistribution(logChan) + return &FedoraDistribution{ + BaseDistribution: base, + ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, + config: config, + } +} + +func (f *FedoraDistribution) GetID() string { + return f.config.ID +} + +func (f *FedoraDistribution) GetColorHex() string { + return f.config.ColorHex +} + +func (f *FedoraDistribution) GetFamily() DistroFamily { + return f.config.Family +} + +func (f *FedoraDistribution) GetPackageManager() PackageManagerType { + return PackageManagerDNF +} + +func (f *FedoraDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return f.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (f *FedoraDistribution) 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, f.detectDMS()) + + // Terminal with choice support + dependencies = append(dependencies, f.detectSpecificTerminal(terminal)) + + // Common detections using base methods + dependencies = append(dependencies, f.detectGit()) + dependencies = append(dependencies, f.detectWindowManager(wm)) + dependencies = append(dependencies, f.detectQuickshell()) + dependencies = append(dependencies, f.detectDMSGreeter()) + dependencies = append(dependencies, f.detectXDGPortal()) + dependencies = append(dependencies, f.detectAccountsService()) + + // Hyprland-specific tools + if wm == deps.WindowManagerHyprland { + dependencies = append(dependencies, f.detectHyprlandTools()...) + } + + // Niri-specific tools + if wm == deps.WindowManagerNiri { + dependencies = append(dependencies, f.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, f.detectMatugen()) + dependencies = append(dependencies, f.detectDgop()) + + return dependencies, nil +} + +func (f *FedoraDistribution) detectXDGPortal() deps.Dependency { + return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (f *FedoraDistribution) packageInstalled(pkg string) bool { + cmd := exec.Command("rpm", "-q", pkg) + err := cmd.Run() + return err == nil +} + +func (f *FedoraDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return f.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + packages := map[string]PackageMapping{ + // Standard DNF packages + "git": {Name: "git", Repository: RepoTypeSystem}, + "ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, + "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}, + + // COPR packages + "quickshell": f.getQuickshellMapping(variants["quickshell"]), + "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, + "matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, + "dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]), + "dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, + } + + switch wm { + case deps.WindowManagerHyprland: + packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"]) + packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"]) + packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} + case deps.WindowManagerNiri: + packages["niri"] = f.getNiriMapping(variants["niri"]) + packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} + } + + return packages +} + +func (f *FedoraDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping { + if forceQuickshellGit || variant == deps.VariantGit { + return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"} + } + return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"} +} + +func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms-git"} + } + return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"} +} + +func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { + return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"} +} + +func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"} + } + return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"} +} + +func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { + status := deps.StatusMissing + if f.commandExists("xwayland-satellite") { + status = deps.StatusInstalled + } + + return deps.Dependency{ + Name: "xwayland-satellite", + Status: status, + Description: "Xwayland support", + Required: true, + } +} + +func (f *FedoraDistribution) detectAccountsService() deps.Dependency { + status := deps.StatusMissing + if f.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 (f *FedoraDistribution) detectDMSGreeter() deps.Dependency { + return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter")) +} + +func (f *FedoraDistribution) getPrerequisites() []string { + return []string{ + "dnf-plugins-core", + "make", + "unzip", + "libwayland-server", + } +} + +func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + prerequisites := f.getPrerequisites() + var missingPkgs []string + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Checking prerequisites...", + IsComplete: false, + LogOutput: "Checking prerequisite packages", + } + + for _, pkg := range prerequisites { + checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg) + if err := checkCmd.Run(); err != nil { + missingPkgs = append(missingPkgs, pkg) + } + } + + _, err := exec.LookPath("go") + if err != nil { + f.log("go not found in PATH, will install golang-bin") + missingPkgs = append(missingPkgs, "golang-bin") + } else { + f.log("go already available in PATH") + } + + if len(missingPkgs) == 0 { + f.log("All prerequisites already installed") + return nil + } + + f.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.08, + Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo dnf install -y %s", strings.Join(missingPkgs, " ")), + LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), + } + + args := []string{"dnf", "install", "-y"} + args = append(args, missingPkgs...) + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + output, err := cmd.CombinedOutput() + if err != nil { + f.logError("failed to install prerequisites", err) + f.log(fmt.Sprintf("Prerequisites command output: %s", string(output))) + return fmt.Errorf("failed to install prerequisites: %w", err) + } + f.log(fmt.Sprintf("Prerequisites install output: %s", string(output))) + + return nil +} + +func (f *FedoraDistribution) 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 := f.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + dnfPkgs, coprPkgs, manualPkgs, variantMap := f.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + // Phase 2: Enable COPR repositories + if len(coprPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.15, + Step: "Enabling COPR repositories...", + IsComplete: false, + LogOutput: "Setting up COPR repositories for additional packages", + } + if err := f.enableCOPRRepos(ctx, coprPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to enable COPR repositories: %w", err) + } + } + + // Phase 3: System Packages (DNF) + if len(dnfPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d system packages...", len(dnfPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(dnfPkgs, ", ")), + } + if err := f.installDNFPackages(ctx, dnfPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install DNF packages: %w", err) + } + } + + // Phase 4: COPR Packages + coprPkgNames := f.extractPackageNames(coprPkgs) + if len(coprPkgNames) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, // Reusing AUR phase for COPR + Progress: 0.65, + Step: fmt.Sprintf("Installing %d COPR packages...", len(coprPkgNames)), + IsComplete: false, + LogOutput: fmt.Sprintf("Installing COPR packages: %s", strings.Join(coprPkgNames, ", ")), + } + if err := f.installCOPRPackages(ctx, coprPkgNames, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install COPR packages: %w", err) + } + } + + // Phase 5: Manual Builds + if len(manualPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.85, + Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), + } + if err := f.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install manual packages: %w", err) + } + } + + // Phase 6: Configuration + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + terminal := f.DetectTerminalFromDeps(dependencies) + if err := f.WriteEnvironmentConfig(terminal); err != nil { + f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := f.WriteWindowManagerConfig(wm); err != nil { + f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := f.EnableDMSService(ctx, wm); err != nil { + f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + // Phase 7: Complete + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { + dnfPkgs := []string{} + coprPkgs := []PackageMapping{} + manualPkgs := []string{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := f.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 { + f.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) + continue + } + + switch pkgInfo.Repository { + case RepoTypeSystem: + dnfPkgs = append(dnfPkgs, pkgInfo.Name) + case RepoTypeCOPR: + coprPkgs = append(coprPkgs, pkgInfo) + case RepoTypeManual: + manualPkgs = append(manualPkgs, dep.Name) + } + } + + return dnfPkgs, coprPkgs, manualPkgs, variantMap +} + +func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string { + names := make([]string, len(packages)) + for i, pkg := range packages { + names[i] = pkg.Name + } + return names +} + +func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + enabledRepos := make(map[string]bool) + + for _, pkg := range coprPkgs { + if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] { + f.log(fmt.Sprintf("Enabling COPR repository: %s", pkg.RepoURL)) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.20, + Step: fmt.Sprintf("Enabling COPR repo %s...", pkg.RepoURL), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL)) + output, err := cmd.CombinedOutput() + if err != nil { + f.logError(fmt.Sprintf("failed to enable COPR repo %s", pkg.RepoURL), err) + f.log(fmt.Sprintf("COPR enable command output: %s", string(output))) + return fmt.Errorf("failed to enable COPR repo %s: %w", pkg.RepoURL, err) + } + f.log(fmt.Sprintf("COPR repo %s enabled successfully: %s", pkg.RepoURL, string(output))) + enabledRepos[pkg.RepoURL] = true + + // Special handling for niri COPR repo - set priority=1 + if pkg.RepoURL == "yalter/niri-git" { + f.log("Setting priority=1 for niri-git COPR repo...") + repoFile := "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo" + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.22, + Step: "Setting niri COPR repo priority...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile), + } + + priorityCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile)) + priorityOutput, err := priorityCmd.CombinedOutput() + if err != nil { + f.logError("failed to set niri COPR repo priority", err) + f.log(fmt.Sprintf("Priority command output: %s", string(priorityOutput))) + return fmt.Errorf("failed to set niri COPR repo priority: %w", err) + } + f.log(fmt.Sprintf("niri COPR repo priority set successfully: %s", string(priorityOutput))) + } + } + } + + return nil +} + +func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) + + return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60) +} + +func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", "))) + + return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85) +} + +func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string { + args := []string{"dnf", "install", "-y"} + if minimal { + args = append(args, "--setopt=install_weak_deps=False") + } + return append(args, packages...) +} + +func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) + + groupIndex := 0 + installGroup := func(groupPackages []string, minimal bool) error { + if len(groupPackages) == 0 { + return nil + } + + groupIndex++ + groupStart := startProgress + groupEnd := endProgress + if totalGroups > 1 { + midpoint := startProgress + ((endProgress - startProgress) / 2) + if groupIndex == 1 { + groupEnd = midpoint + } else { + groupStart = midpoint + } + } + + args := f.dnfInstallArgs(groupPackages, minimal) + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: groupStart, + Step: step, + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) + } + + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/gentoo.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/gentoo.go new file mode 100644 index 0000000..e0a2a9c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/gentoo.go @@ -0,0 +1,700 @@ +package distros + +import ( + "context" + "fmt" + "os/exec" + "runtime" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +var GentooGlobalUseFlags = []string{ + "dbus", + "udev", + "alsa", + "policykit", + "jpeg", + "png", + "webp", + "gif", + "tiff", + "svg", + "brotli", + "gdbm", + "accessibility", + "gtk", + "qt6", + "egl", + "gbm", +} + +func init() { + Register("gentoo", "#54487A", FamilyGentoo, func(config DistroConfig, logChan chan<- string) Distribution { + return NewGentooDistribution(config, logChan) + }) +} + +type GentooDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig + skipGlobalUseFlags bool +} + +func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDistribution { + base := NewBaseDistribution(logChan) + return &GentooDistribution{ + BaseDistribution: base, + ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, + config: config, + } +} + +func (g *GentooDistribution) getArchKeyword() string { + arch := runtime.GOARCH + switch arch { + case "amd64": + return "~amd64" + case "arm64": + return "~arm64" + default: + return "~amd64" + } +} + +func (g *GentooDistribution) GetID() string { + return g.config.ID +} + +func (g *GentooDistribution) GetColorHex() string { + return g.config.ColorHex +} + +func (g *GentooDistribution) GetFamily() DistroFamily { + return g.config.Family +} + +func (g *GentooDistribution) GetPackageManager() PackageManagerType { + return PackageManagerPortage +} + +func (g *GentooDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return g.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) { + var dependencies []deps.Dependency + + dependencies = append(dependencies, g.detectDMS()) + + dependencies = append(dependencies, g.detectSpecificTerminal(terminal)) + + dependencies = append(dependencies, g.detectGit()) + dependencies = append(dependencies, g.detectWindowManager(wm)) + dependencies = append(dependencies, g.detectQuickshell()) + dependencies = append(dependencies, g.detectXDGPortal()) + dependencies = append(dependencies, g.detectAccountsService()) + + if wm == deps.WindowManagerHyprland { + dependencies = append(dependencies, g.detectHyprlandTools()...) + } + + if wm == deps.WindowManagerNiri { + dependencies = append(dependencies, g.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, g.detectMatugen()) + dependencies = append(dependencies, g.detectDgop()) + + return dependencies, nil +} + +func (g *GentooDistribution) detectXDGPortal() deps.Dependency { + return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk")) +} + +func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { + return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite")) +} + +func (g *GentooDistribution) detectAccountsService() deps.Dependency { + return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice")) +} + +func (g *GentooDistribution) packageInstalled(pkg string) bool { + cmd := exec.Command("qlist", "-I", pkg) + err := cmd.Run() + return err == nil +} + +func (g *GentooDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return g.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + archKeyword := g.getArchKeyword() + packages := map[string]PackageMapping{ + "git": {Name: "dev-vcs/git", Repository: RepoTypeSystem}, + "kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, + "alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, + "xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"}, + "accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem}, + + "qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"}, + "qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"}, + "qtwayland": {Name: "dev-qt/qtwayland", Repository: RepoTypeSystem}, + "mesa": {Name: "media-libs/mesa", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"}, + + "quickshell": g.getQuickshellMapping(variants["quickshell"]), + "matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, + "dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]), + "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, + } + + switch wm { + case deps.WindowManagerHyprland: + packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"]) + packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"]) + packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem} + case deps.WindowManagerNiri: + packages["niri"] = g.getNiriMapping(variants["niri"]) + packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword} + } + + return packages +} + +func (g *GentooDistribution) getQuickshellMapping(_ deps.PackageVariant) PackageMapping { + return PackageMapping{Name: "gui-apps/quickshell", Repository: RepoTypeGURU, UseFlags: "breakpad jemalloc sockets wayland layer-shell session-lock toplevel-management screencopy X pipewire tray mpris pam hyprland hyprland-global-shortcuts hyprland-focus-grab i3 i3-ipc bluetooth", AcceptKeywords: "**"} +} + +func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping { + return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"} +} + +func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { + return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()} +} + +func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping { + return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()} +} + +func (g *GentooDistribution) getPrerequisites() []string { + return []string{ + "app-eselect/eselect-repository", + "dev-vcs/git", + "dev-build/make", + "app-arch/unzip", + "dev-util/pkgconf", + "dev-qt/qtdeclarative", + } +} + +func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword string) error { + useFlags := strings.Join(GentooGlobalUseFlags, " ") + + checkCmd := exec.CommandContext(ctx, "grep", "-q", "^USE=", "/etc/portage/make.conf") + hasUse := checkCmd.Run() == nil + + var cmd *exec.Cmd + if hasUse { + cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags)) + } else { + cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags)) + } + + output, err := cmd.CombinedOutput() + if err != nil { + g.log(fmt.Sprintf("Failed to set global USE flags: %s", string(output))) + return err + } + + g.log(fmt.Sprintf("Set global USE flags: %s", useFlags)) + return nil +} + +func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + prerequisites := g.getPrerequisites() + var missingPkgs []string + + if !g.skipGlobalUseFlags { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.05, + Step: "Setting global USE flags...", + IsComplete: false, + LogOutput: "Configuring global USE flags in /etc/portage/make.conf", + } + + if err := g.setGlobalUseFlags(ctx, sudoPassword); err != nil { + g.logError("failed to set global USE flags", err) + return fmt.Errorf("failed to set global USE flags: %w", err) + } + } else { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.05, + Step: "Skipping global USE flags...", + IsComplete: false, + LogOutput: "Skipping global USE flags configuration (using existing configuration)", + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Checking prerequisites...", + IsComplete: false, + LogOutput: "Checking prerequisite packages", + } + + for _, pkg := range prerequisites { + checkCmd := exec.CommandContext(ctx, "qlist", "-I", pkg) + if err := checkCmd.Run(); err != nil { + missingPkgs = append(missingPkgs, pkg) + } + } + + _, err := exec.LookPath("go") + if err != nil { + g.log("go not found in PATH, will install dev-lang/go") + missingPkgs = append(missingPkgs, "dev-lang/go") + } else { + g.log("go already available in PATH") + } + + if len(missingPkgs) == 0 { + g.log("All prerequisites already installed") + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.07, + Step: "Syncing Portage tree...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo emerge --sync", + LogOutput: "Syncing Portage tree with emerge --sync", + } + + syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet") + syncOutput, syncErr := syncCmd.CombinedOutput() + if syncErr != nil { + g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput))) + return fmt.Errorf("failed to sync Portage tree: %w\nOutput: %s", syncErr, string(syncOutput)) + } + g.log("Portage tree synced successfully") + + g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.08, + Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")), + LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), + } + + args := []string{"emerge", "--ask=n", "--quiet"} + args = append(args, missingPkgs...) + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + output, err := cmd.CombinedOutput() + if err != nil { + g.logError("failed to install prerequisites", err) + g.log(fmt.Sprintf("Prerequisites command output: %s", string(output))) + return fmt.Errorf("failed to install prerequisites: %w\nOutput: %s", err, string(output)) + } + g.log(fmt.Sprintf("Prerequisites install output: %s", string(output))) + + return nil +} + +func (g *GentooDistribution) 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 { + g.skipGlobalUseFlags = skipGlobalUseFlags + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.05, + Step: "Checking system prerequisites...", + IsComplete: false, + LogOutput: "Starting prerequisite check...", + } + + if err := g.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + systemPkgs, guruPkgs, manualPkgs, variantMap := g.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs))) + + if len(systemPkgs) > 0 { + systemPkgNames := g.extractPackageNames(systemPkgs) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgNames, ", ")), + } + if err := g.installPortagePackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install Portage packages: %w", err) + } + } + + if len(guruPkgs) > 0 { + g.log(fmt.Sprintf("FOUND %d GURU PACKAGES - WILL SYNC GURU REPO", len(guruPkgs))) + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.60, + Step: "Syncing GURU repository...", + IsComplete: false, + LogOutput: "Syncing GURU repository to fetch latest ebuilds", + } + g.log("ABOUT TO CALL syncGURURepo") + if err := g.syncGURURepo(ctx, sudoPassword, progressChan); err != nil { + g.log(fmt.Sprintf("syncGURURepo RETURNED ERROR: %v", err)) + return fmt.Errorf("failed to sync GURU repository: %w", err) + } + g.log("syncGURURepo COMPLETED SUCCESSFULLY") + + guruPkgNames := g.extractPackageNames(guruPkgs) + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.65, + Step: fmt.Sprintf("Installing %d GURU packages...", len(guruPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Installing GURU packages: %s", strings.Join(guruPkgNames, ", ")), + } + if err := g.installGURUPackages(ctx, guruPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install GURU packages: %w", err) + } + } + + if len(manualPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.85, + Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), + } + if err := g.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install manual packages: %w", err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + terminal := g.DetectTerminalFromDeps(dependencies) + if err := g.WriteEnvironmentConfig(terminal); err != nil { + g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := g.WriteWindowManagerConfig(wm); err != nil { + g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := g.EnableDMSService(ctx, wm); err != nil { + g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string, map[string]deps.PackageVariant) { + systemPkgs := []PackageMapping{} + guruPkgs := []PackageMapping{} + manualPkgs := []string{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := g.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 { + g.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) + continue + } + + switch pkgInfo.Repository { + case RepoTypeSystem: + systemPkgs = append(systemPkgs, pkgInfo) + case RepoTypeGURU: + guruPkgs = append(guruPkgs, pkgInfo) + case RepoTypeManual: + manualPkgs = append(manualPkgs, dep.Name) + } + } + + return systemPkgs, guruPkgs, manualPkgs, variantMap +} + +func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string { + names := make([]string, len(packages)) + for i, pkg := range packages { + names[i] = pkg.Name + } + return names +} + +func (g *GentooDistribution) installPortagePackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + packageNames := g.extractPackageNames(packages) + g.log(fmt.Sprintf("Installing Portage packages: %s", strings.Join(packageNames, ", "))) + + for _, pkg := range packages { + if pkg.AcceptKeywords != "" { + if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil { + return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err) + } + } + if pkg.UseFlags != "" { + if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil { + return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err) + } + } + } + + args := []string{"emerge", "--ask=n", "--quiet"} + args = append(args, packageNames...) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.40, + Step: "Installing system packages...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0) +} + +func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error { + packageUseDir := "/etc/portage/package.use" + + mkdirCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("mkdir -p %s", packageUseDir)) + if output, err := mkdirCmd.CombinedOutput(); err != nil { + g.log(fmt.Sprintf("mkdir output: %s", string(output))) + return fmt.Errorf("failed to create package.use directory: %w", err) + } + + useFlagLine := fmt.Sprintf("%s %s", packageName, useFlags) + + checkExistingCmd := exec.CommandContext(ctx, "bash", "-c", + fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, packageUseDir)) + if checkExistingCmd.Run() == nil { + g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName)) + escapedPkg := strings.ReplaceAll(packageName, "/", "\\/") + replaceCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir)) + if output, err := replaceCmd.CombinedOutput(); err != nil { + g.log(fmt.Sprintf("sed delete output: %s", string(output))) + return fmt.Errorf("failed to remove old USE flags: %w", err) + } + } + + appendCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir)) + + output, err := appendCmd.CombinedOutput() + if err != nil { + g.log(fmt.Sprintf("append output: %s", string(output))) + return fmt.Errorf("failed to write USE flags to package.use: %w", err) + } + + g.log(fmt.Sprintf("Set USE flags for %s: %s", packageName, useFlags)) + return nil +} + +func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.55, + Step: "Enabling GURU repository...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo eselect repository enable guru", + LogOutput: "Enabling GURU repository with eselect", + } + + // Enable GURU repository + enableCmd := ExecSudoCommand(ctx, sudoPassword, + "eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code") + output, err := enableCmd.CombinedOutput() + + g.log(fmt.Sprintf("eselect repository enable guru output:\n%s", string(output))) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.55, + LogOutput: "GURU repository enabled", + } + + if err != nil { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.55, + LogOutput: fmt.Sprintf("ERROR enabling GURU: %v", err), + Error: err, + } + return fmt.Errorf("failed to enable GURU repository: %w\nOutput: %s", err, string(output)) + } + + // Sync GURU repository + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.57, + Step: "Syncing GURU repository...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo emaint sync --repo guru", + LogOutput: "Syncing GURU repository", + } + + syncCmd := ExecSudoCommand(ctx, sudoPassword, + "emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code") + syncOutput, syncErr := syncCmd.CombinedOutput() + + g.log(fmt.Sprintf("emaint sync --repo guru output:\n%s", string(syncOutput))) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.57, + LogOutput: "GURU repository synced", + } + + if syncErr != nil { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.57, + LogOutput: fmt.Sprintf("ERROR syncing GURU: %v", syncErr), + Error: syncErr, + } + return fmt.Errorf("failed to sync GURU repository: %w\nOutput: %s", syncErr, string(syncOutput)) + } + + return nil +} + +func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packageName, keywords, sudoPassword string) error { + checkCmd := exec.CommandContext(ctx, "portageq", "match", "/", packageName) + if output, err := checkCmd.CombinedOutput(); err == nil && len(output) > 0 { + g.log(fmt.Sprintf("Package %s is already available (may already be unmasked)", packageName)) + return nil + } + + acceptKeywordsDir := "/etc/portage/package.accept_keywords" + + mkdirCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("mkdir -p %s", acceptKeywordsDir)) + if output, err := mkdirCmd.CombinedOutput(); err != nil { + g.log(fmt.Sprintf("mkdir output: %s", string(output))) + return fmt.Errorf("failed to create package.accept_keywords directory: %w", err) + } + + keywordLine := fmt.Sprintf("%s %s", packageName, keywords) + + checkExistingCmd := exec.CommandContext(ctx, "bash", "-c", + fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, acceptKeywordsDir)) + if checkExistingCmd.Run() == nil { + g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName)) + escapedPkg := strings.ReplaceAll(packageName, "/", "\\/") + replaceCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir)) + if output, err := replaceCmd.CombinedOutput(); err != nil { + g.log(fmt.Sprintf("sed delete output: %s", string(output))) + return fmt.Errorf("failed to remove old accept keywords: %w", err) + } + } + + appendCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir)) + + output, err := appendCmd.CombinedOutput() + if err != nil { + g.log(fmt.Sprintf("append output: %s", string(output))) + return fmt.Errorf("failed to write accept keywords: %w", err) + } + + g.log(fmt.Sprintf("Set accept keywords for %s: %s", packageName, keywords)) + return nil +} + +func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + packageNames := g.extractPackageNames(packages) + g.log(fmt.Sprintf("Installing GURU packages: %s", strings.Join(packageNames, ", "))) + + for _, pkg := range packages { + if pkg.AcceptKeywords != "" { + if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil { + return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err) + } + } + if pkg.UseFlags != "" { + if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil { + return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err) + } + } + } + + guruPackages := make([]string, len(packageNames)) + for i, pkg := range packageNames { + guruPackages[i] = pkg + "::guru" + } + + args := []string{"emerge", "--ask=n", "--quiet"} + args = append(args, guruPackages...) + + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.70, + Step: "Installing GURU packages...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/interface.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/interface.go new file mode 100644 index 0000000..f719f1b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/interface.go @@ -0,0 +1,158 @@ +package distros + +import ( + "context" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +// DistroFamily represents a family of related distributions +type DistroFamily string + +const ( + FamilyArch DistroFamily = "arch" + FamilyFedora DistroFamily = "fedora" + FamilySUSE DistroFamily = "suse" + FamilyUbuntu DistroFamily = "ubuntu" + FamilyDebian DistroFamily = "debian" + FamilyNix DistroFamily = "nix" + FamilyGentoo DistroFamily = "gentoo" +) + +// PackageManagerType defines the package manager a distro uses +type PackageManagerType string + +const ( + PackageManagerPacman PackageManagerType = "pacman" + PackageManagerDNF PackageManagerType = "dnf" + PackageManagerAPT PackageManagerType = "apt" + PackageManagerZypper PackageManagerType = "zypper" + PackageManagerNix PackageManagerType = "nix" + PackageManagerPortage PackageManagerType = "portage" +) + +// RepositoryType defines the type of repository for a package +type RepositoryType string + +const ( + RepoTypeSystem RepositoryType = "system" // Standard system repo (pacman, dnf, apt) + RepoTypeAUR RepositoryType = "aur" // Arch User Repository + RepoTypeCOPR RepositoryType = "copr" // Fedora COPR + RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA + RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE) + RepoTypeFlake RepositoryType = "flake" // Nix flake + RepoTypeGURU RepositoryType = "guru" // Gentoo GURU + RepoTypeManual RepositoryType = "manual" // Manual build from source +) + +// InstallPhase represents the current phase of installation +type InstallPhase int + +const ( + PhasePrerequisites InstallPhase = iota + PhaseAURHelper + PhaseSystemPackages + PhaseAURPackages + PhaseCursorTheme + PhaseConfiguration + PhaseGreeterSetup + PhaseComplete +) + +// InstallProgressMsg represents progress during package installation +type InstallProgressMsg struct { + Phase InstallPhase + Progress float64 + Step string + IsComplete bool + NeedsSudo bool + CommandInfo string + LogOutput string + Error error +} + +// PackageMapping defines how to install a package on a specific distro +type PackageMapping struct { + Name string // Package name to install + Repository RepositoryType // Repository type + RepoURL string // Repository URL if needed (e.g., COPR repo, PPA) + BuildFunc string // Name of manual build function if RepoTypeManual + UseFlags string // USE flags for Gentoo packages + AcceptKeywords string // Accept keywords for Gentoo packages (e.g., "~amd64") +} + +// Distribution defines a Linux distribution with all its specific configurations +type Distribution interface { + // Metadata + GetID() string + GetColorHex() string + GetFamily() DistroFamily + GetPackageManager() PackageManagerType + + // Dependency Detection + DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) + DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) + + // Package Installation + 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 + + // Package Mapping + GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping + + // Prerequisites + InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error +} + +// DistroConfig holds configuration for a distribution +type DistroConfig struct { + ID string + ColorHex string + Family DistroFamily + Constructor func(config DistroConfig, logChan chan<- string) Distribution +} + +// Registry holds all supported distributions +var Registry = make(map[string]DistroConfig) + +// Register adds a distribution to the registry +func Register(id, colorHex string, family DistroFamily, constructor func(config DistroConfig, logChan chan<- string) Distribution) { + Registry[id] = DistroConfig{ + ID: id, + ColorHex: colorHex, + Family: family, + Constructor: constructor, + } +} + +// GetSupportedDistros returns a list of all supported distribution IDs +func GetSupportedDistros() []string { + ids := make([]string, 0, len(Registry)) + for id := range Registry { + ids = append(ids, id) + } + return ids +} + +// IsDistroSupported checks if a distribution ID is supported +func IsDistroSupported(id string) bool { + _, exists := Registry[id] + return exists +} + +// NewDistribution creates a distribution instance by ID +func NewDistribution(id string, logChan chan<- string) (Distribution, error) { + config, exists := Registry[id] + if !exists { + return nil, &UnsupportedDistributionError{ID: id} + } + return config.Constructor(config, logChan), nil +} + +// UnsupportedDistributionError is returned when a distribution is not supported +type UnsupportedDistributionError struct { + ID string +} + +func (e *UnsupportedDistributionError) Error() string { + return "unsupported distribution: " + e.ID +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages.go new file mode 100644 index 0000000..2f38161 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages.go @@ -0,0 +1,663 @@ +package distros + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +// ManualPackageInstaller provides methods for installing packages from source +type ManualPackageInstaller struct { + *BaseDistribution +} + +// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag +func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string { + lines := strings.SplitSeq(output, "\n") + for line := range lines { + if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") { + parts := strings.Split(line, "refs/tags/") + if len(parts) > 1 { + latestTag := strings.TrimSpace(parts[1]) + return latestTag + } + } + } + return "" +} + +// getLatestQuickshellTag fetches the latest tag from the quickshell repository +func (m *ManualPackageInstaller) getLatestQuickshellTag(ctx context.Context) string { + tagCmd := exec.CommandContext(ctx, "git", "ls-remote", "--tags", "--sort=-v:refname", + "https://github.com/quickshell-mirror/quickshell.git") + tagOutput, err := tagCmd.Output() + if err != nil { + m.log(fmt.Sprintf("Warning: failed to fetch quickshell tags: %v", err)) + return "" + } + + return m.parseLatestTagFromGitOutput(string(tagOutput)) +} + +func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", "))) + + for _, pkg := range packages { + variant := variantMap[pkg] + switch pkg { + case "dms (DankMaterialShell)", "dms": + if err := m.installDankMaterialShell(ctx, variant, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install DankMaterialShell: %w", err) + } + case "dgop": + if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install dgop: %w", err) + } + case "niri": + if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install niri: %w", err) + } + case "quickshell": + if err := m.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install quickshell: %w", err) + } + case "hyprland": + if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install hyprland: %w", err) + } + case "ghostty": + if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install ghostty: %w", err) + } + case "matugen": + if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install matugen: %w", err) + } + case "xwayland-satellite": + if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install xwayland-satellite: %w", err) + } + default: + m.log(fmt.Sprintf("Warning: No manual build method for %s", pkg)) + } + } + + return nil +} + +func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing dgop from source...") + + homeDir := os.Getenv("HOME") + if homeDir == "" { + return fmt.Errorf("HOME environment variable not set") + } + + cacheDir := filepath.Join(homeDir, ".cache", "dankinstall") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + tmpDir := filepath.Join(cacheDir, "dgop-build") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Cloning dgop repository...", + IsComplete: false, + CommandInfo: "git clone https://github.com/AvengeMedia/dgop.git", + } + + cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/AvengeMedia/dgop.git", tmpDir) + if err := cloneCmd.Run(); err != nil { + m.logError("failed to clone dgop repository", err) + return fmt.Errorf("failed to clone dgop repository: %w", err) + } + + buildCmd := exec.CommandContext(ctx, "make") + buildCmd.Dir = tmpDir + buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) + if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.7, "Building dgop..."); err != nil { + return fmt.Errorf("failed to build dgop: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.7, + Step: "Installing dgop...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo make install", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, "make install") + installCmd.Dir = tmpDir + if err := installCmd.Run(); err != nil { + m.logError("failed to install dgop", err) + return fmt.Errorf("failed to install dgop: %w", err) + } + + m.log("dgop installed successfully from source") + return nil +} + +func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing niri from source...") + + homeDir, _ := os.UserHomeDir() + buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build") + tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp") + if err := os.MkdirAll(buildDir, 0o755); err != nil { + return fmt.Errorf("failed to create build directory: %w", err) + } + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer func() { + os.RemoveAll(buildDir) + os.RemoveAll(tmpDir) + }() + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.2, + Step: "Cloning niri repository...", + IsComplete: false, + CommandInfo: "git clone https://github.com/YaLTeR/niri.git", + } + + cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/YaLTeR/niri.git", buildDir) + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone niri: %w", err) + } + + checkoutCmd := exec.CommandContext(ctx, "git", "-C", buildDir, "checkout", "v25.08") + if err := checkoutCmd.Run(); err != nil { + m.log(fmt.Sprintf("Warning: failed to checkout v25.08, using main: %v", err)) + } + + if !m.commandExists("cargo-deb") { + cargoDebInstallCmd := exec.CommandContext(ctx, "cargo", "install", "cargo-deb") + cargoDebInstallCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir) + if err := m.runWithProgressStep(cargoDebInstallCmd, progressChan, PhaseSystemPackages, 0.3, 0.35, "Installing cargo-deb..."); err != nil { + return fmt.Errorf("failed to install cargo-deb: %w", err) + } + } + + buildDebCmd := exec.CommandContext(ctx, "cargo", "deb") + buildDebCmd.Dir = buildDir + buildDebCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir) + if err := m.runWithProgressStep(buildDebCmd, progressChan, PhaseSystemPackages, 0.35, 0.95, "Building niri deb package..."); err != nil { + return fmt.Errorf("failed to build niri deb: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.95, + Step: "Installing niri deb package...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "dpkg -i niri.deb", + } + + installDebCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir)) + + output, err := installDebCmd.CombinedOutput() + if err != nil { + m.log(fmt.Sprintf("dpkg install failed. Output:\n%s", string(output))) + return fmt.Errorf("failed to install niri deb package: %w\nOutput:\n%s", err, string(output)) + } + + m.log(fmt.Sprintf("dpkg install successful. Output:\n%s", string(output))) + + m.log("niri installed successfully from source") + return nil +} + +func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing quickshell from source...") + + homeDir := os.Getenv("HOME") + if homeDir == "" { + return fmt.Errorf("HOME environment variable not set") + } + + cacheDir := filepath.Join(homeDir, ".cache", "dankinstall") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + tmpDir := filepath.Join(cacheDir, "quickshell-build") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Cloning quickshell repository...", + IsComplete: false, + CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git", + } + + var cloneCmd *exec.Cmd + if forceQuickshellGit || variant == deps.VariantGit { + cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir) + } else { + latestTag := m.getLatestQuickshellTag(ctx) + if latestTag != "" { + m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag)) + cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir) + } else { + m.log("Warning: failed to fetch latest tag, using default branch") + cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir) + } + } + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone quickshell: %w", err) + } + + buildDir := tmpDir + "/build" + if err := os.MkdirAll(buildDir, 0o755); err != nil { + return fmt.Errorf("failed to create build directory: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.3, + Step: "Configuring quickshell build...", + IsComplete: false, + CommandInfo: "cmake -B build -S . -G Ninja", + } + + configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build", + "-DCMAKE_BUILD_TYPE=RelWithDebInfo", + "-DCRASH_REPORTER=off", + "-DCMAKE_CXX_STANDARD=20") + configureCmd.Dir = tmpDir + configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) + + output, err := configureCmd.CombinedOutput() + if err != nil { + m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output))) + return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output)) + } + + m.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output))) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.4, + Step: "Building quickshell (this may take a while)...", + IsComplete: false, + CommandInfo: "cmake --build build", + } + + buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build") + buildCmd.Dir = tmpDir + buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) + if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil { + return fmt.Errorf("failed to build quickshell: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.8, + Step: "Installing quickshell...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo cmake --install build", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build") + installCmd.Dir = tmpDir + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install quickshell: %w", err) + } + + m.log("quickshell installed successfully from source") + return nil +} + +func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing Hyprland from source...") + + homeDir := os.Getenv("HOME") + if homeDir == "" { + return fmt.Errorf("HOME environment variable not set") + } + + cacheDir := filepath.Join(homeDir, ".cache", "dankinstall") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + tmpDir := filepath.Join(cacheDir, "hyprland-build") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Cloning Hyprland repository...", + IsComplete: false, + CommandInfo: "git clone --recursive https://github.com/hyprwm/Hyprland.git", + } + + cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recursive", "https://github.com/hyprwm/Hyprland.git", tmpDir) + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone Hyprland: %w", err) + } + + checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "v0.50.1") + if err := checkoutCmd.Run(); err != nil { + m.log(fmt.Sprintf("Warning: failed to checkout v0.50.1, using main: %v", err)) + } + + buildCmd := exec.CommandContext(ctx, "make", "all") + buildCmd.Dir = tmpDir + buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) + if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.2, 0.8, "Building Hyprland..."); err != nil { + return fmt.Errorf("failed to build Hyprland: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.8, + Step: "Installing Hyprland...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo make install", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, "make install") + installCmd.Dir = tmpDir + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install Hyprland: %w", err) + } + + m.log("Hyprland installed successfully from source") + return nil +} + +func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing Ghostty from source...") + + homeDir := os.Getenv("HOME") + if homeDir == "" { + return fmt.Errorf("HOME environment variable not set") + } + + cacheDir := filepath.Join(homeDir, ".cache", "dankinstall") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + tmpDir := filepath.Join(cacheDir, "ghostty-build") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Cloning Ghostty repository...", + IsComplete: false, + CommandInfo: "git clone https://github.com/ghostty-org/ghostty.git", + } + + cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/ghostty-org/ghostty.git", tmpDir) + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone Ghostty: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.2, + Step: "Building Ghostty (this may take a while)...", + IsComplete: false, + CommandInfo: "zig build -Doptimize=ReleaseFast", + } + + buildCmd := exec.CommandContext(ctx, "zig", "build", "-Doptimize=ReleaseFast") + buildCmd.Dir = tmpDir + buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir) + if err := buildCmd.Run(); err != nil { + return fmt.Errorf("failed to build Ghostty: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.8, + Step: "Installing Ghostty...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir)) + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install Ghostty: %w", err) + } + + m.log("Ghostty installed successfully from source") + return nil +} + +func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing matugen from source...") + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Installing matugen via cargo...", + IsComplete: false, + CommandInfo: "cargo install matugen", + } + + installCmd := exec.CommandContext(ctx, "cargo", "install", "matugen") + if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building matugen..."); err != nil { + return fmt.Errorf("failed to install matugen: %w", err) + } + + homeDir := os.Getenv("HOME") + sourcePath := filepath.Join(homeDir, ".cargo", "bin", "matugen") + targetPath := "/usr/local/bin/matugen" + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.7, + Step: "Installing matugen binary to system...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath), + } + + copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath) + copyCmd.Stdin = strings.NewReader(sudoPassword + "\n") + if err := copyCmd.Run(); err != nil { + return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err) + } + + // Make it executable + chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath) + chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n") + if err := chmodCmd.Run(); err != nil { + return fmt.Errorf("failed to make matugen executable: %w", err) + } + + m.log("matugen installed successfully from source") + return nil +} + +func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing DankMaterialShell (DMS)...") + + if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil { + m.logError("Failed to install DMS binary", err) + } + + dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms") + + if _, err := os.Stat(dmsPath); os.IsNotExist(err) { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.90, + Step: "Cloning DankMaterialShell...", + IsComplete: false, + CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git", + } + + configDir := filepath.Dir(dmsPath) + if err := os.MkdirAll(configDir, 0o755); err != nil { + return fmt.Errorf("failed to create quickshell config directory: %w", err) + } + + cloneCmd := exec.CommandContext(ctx, "git", "clone", + "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath) + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone DankMaterialShell: %w", err) + } + + if forceDMSGit || variant == deps.VariantGit { + m.log("Using git variant (master branch)") + return nil + } + + tagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master") + tagOutput, err := tagCmd.Output() + if err != nil { + m.log("Using default branch (no tags found)") + return nil + } + + latestTag := strings.TrimSpace(string(tagOutput)) + checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag) + if err := checkoutCmd.Run(); err != nil { + m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err) + return nil + } + + m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag)) + m.log("DankMaterialShell cloned successfully") + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.90, + Step: "Updating DankMaterialShell...", + IsComplete: false, + CommandInfo: "Updating ~/.config/quickshell/dms", + } + + fetchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "fetch", "origin", "--tags", "--force") + if err := fetchCmd.Run(); err != nil { + m.logError("Failed to fetch updates", err) + return nil + } + + if forceDMSGit || variant == deps.VariantGit { + branchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "rev-parse", "--abbrev-ref", "HEAD") + branchOutput, err := branchCmd.Output() + if err != nil { + m.logError("Failed to get current branch", err) + return nil + } + + branch := strings.TrimSpace(string(branchOutput)) + if branch == "" { + branch = "master" + } + + pullCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "pull", "origin", branch) + if err := pullCmd.Run(); err != nil { + m.logError("Failed to pull updates", err) + return nil + } + + m.log("DankMaterialShell updated successfully (git variant)") + return nil + } + + latestTagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master") + tagOutput, err := latestTagCmd.Output() + if err != nil { + m.logError("Failed to get latest tag", err) + return nil + } + + latestTag := strings.TrimSpace(string(tagOutput)) + checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag) + if err := checkoutCmd.Run(); err != nil { + m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err) + return nil + } + + m.log(fmt.Sprintf("Updated to tag: %s", latestTag)) + return nil +} + +func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + m.log("Installing xwayland-satellite from source...") + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Installing xwayland-satellite via cargo...", + IsComplete: false, + CommandInfo: "cargo install --git https://github.com/Supreeeme/xwayland-satellite --tag v0.7", + } + + installCmd := exec.CommandContext(ctx, "cargo", "install", "--git", "https://github.com/Supreeeme/xwayland-satellite", "--tag", "v0.7") + if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building xwayland-satellite..."); err != nil { + return fmt.Errorf("failed to install xwayland-satellite: %w", err) + } + + homeDir := os.Getenv("HOME") + sourcePath := filepath.Join(homeDir, ".cargo", "bin", "xwayland-satellite") + targetPath := "/usr/local/bin/xwayland-satellite" + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.7, + Step: "Installing xwayland-satellite binary to system...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath), + } + + copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath) + copyCmd.Stdin = strings.NewReader(sudoPassword + "\n") + if err := copyCmd.Run(); err != nil { + return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err) + } + + chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath) + chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n") + if err := chmodCmd.Run(); err != nil { + return fmt.Errorf("failed to make xwayland-satellite executable: %w", err) + } + + m.log("xwayland-satellite installed successfully from source") + return nil +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages_test.go new file mode 100644 index 0000000..4d9fc62 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages_test.go @@ -0,0 +1,122 @@ +package distros + +import ( + "testing" +) + +func TestManualPackageInstaller_parseLatestTagFromGitOutput(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "normal tag output", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1 +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0 +703a3789083d2f990c4e99cd25c97c2a4cccbd81 refs/tags/v0.1.0`, + expected: "v0.2.1", + }, + { + name: "annotated tags with ^{}", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1 +b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.2.1^{} +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`, + expected: "v0.2.1", + }, + { + name: "mixed tags", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0 +b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.3.0^{} +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0 +c1c150fab00a93ea983aaca5df55304bc837f51d refs/tags/beta-1`, + expected: "v0.3.0", + }, + { + name: "empty output", + input: "", + expected: "", + }, + { + name: "no tags", + input: "some other output\nwithout tags", + expected: "", + }, + { + name: "only annotated tags", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1^{} +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0^{}`, + expected: "", + }, + { + name: "single tag", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.0.0`, + expected: "v1.0.0", + }, + { + name: "tag with extra whitespace", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1 +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`, + expected: "v0.2.1", + }, + { + name: "beta and rc tags", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0-beta.1 +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`, + expected: "v0.3.0-beta.1", + }, + { + name: "tags without v prefix", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/0.2.1 +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/0.2.0`, + expected: "0.2.1", + }, + { + name: "multiple lines with spaces", + input: ` +a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.2.3 +a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v1.2.2 +`, + expected: "v1.2.3", + }, + { + name: "tag at end of line", + input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1`, + expected: "v0.2.1", + }, + } + + logChan := make(chan string, 100) + defer close(logChan) + + base := NewBaseDistribution(logChan) + installer := &ManualPackageInstaller{BaseDistribution: base} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := installer.parseLatestTagFromGitOutput(tt.input) + + if result != tt.expected { + t.Errorf("parseLatestTagFromGitOutput() = %q, expected %q", result, tt.expected) + } + }) + } +} + +func TestManualPackageInstaller_parseLatestTagFromGitOutput_EmptyInstaller(t *testing.T) { + // Test that parsing works even with a minimal installer setup + logChan := make(chan string, 10) + defer close(logChan) + + base := NewBaseDistribution(logChan) + installer := &ManualPackageInstaller{BaseDistribution: base} + + input := `abc123 refs/tags/v1.0.0 +def456 refs/tags/v0.9.0` + + result := installer.parseLatestTagFromGitOutput(input) + + if result != "v1.0.0" { + t.Errorf("Expected v1.0.0, got %s", result) + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/minimal_install.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/minimal_install.go new file mode 100644 index 0000000..029f1ba --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/minimal_install.go @@ -0,0 +1,44 @@ +package distros + +type minimalInstallGroup struct { + packages []string + minimal bool +} + +func shouldPreferMinimalInstall(pkg string) bool { + switch pkg { + case "niri", "niri-git": + return true + default: + return false + } +} + +func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) { + for _, pkg := range packages { + if shouldPreferMinimalInstall(pkg) { + minimal = append(minimal, pkg) + continue + } + normal = append(normal, pkg) + } + return normal, minimal +} + +func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup { + normal, minimal := splitMinimalInstallPackages(packages) + groups := make([]minimalInstallGroup, 0, 2) + if len(minimal) > 0 { + groups = append(groups, minimalInstallGroup{ + packages: minimal, + minimal: true, + }) + } + if len(normal) > 0 { + groups = append(groups, minimalInstallGroup{ + packages: normal, + minimal: false, + }) + } + return groups +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go new file mode 100644 index 0000000..62f40cd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go @@ -0,0 +1,856 @@ +package distros + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +func init() { + Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution { + return NewOpenSUSEDistribution(config, logChan) + }) + Register("opensuse-leap", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution { + return NewOpenSUSEDistribution(config, logChan) + }) + Register("opensuse-slowroll", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution { + return NewOpenSUSEDistribution(config, logChan) + }) +} + +type OpenSUSEDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig +} + +const openSUSENiriWaylandServerPackage = "libwayland-server0" + +func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { + base := NewBaseDistribution(logChan) + return &OpenSUSEDistribution{ + BaseDistribution: base, + ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, + config: config, + } +} + +func (o *OpenSUSEDistribution) GetID() string { + return o.config.ID +} + +func (o *OpenSUSEDistribution) GetColorHex() string { + return o.config.ColorHex +} + +func (o *OpenSUSEDistribution) GetFamily() DistroFamily { + return o.config.Family +} + +func (o *OpenSUSEDistribution) GetPackageManager() PackageManagerType { + return PackageManagerZypper +} + +func (o *OpenSUSEDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return o.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (o *OpenSUSEDistribution) 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, o.detectDMS()) + + // Terminal with choice support + dependencies = append(dependencies, o.detectSpecificTerminal(terminal)) + + // Common detections using base methods + dependencies = append(dependencies, o.detectGit()) + dependencies = append(dependencies, o.detectWindowManager(wm)) + dependencies = append(dependencies, o.detectQuickshell()) + dependencies = append(dependencies, o.detectDMSGreeter()) + dependencies = append(dependencies, o.detectXDGPortal()) + dependencies = append(dependencies, o.detectAccountsService()) + + // Hyprland-specific tools + if wm == deps.WindowManagerHyprland { + dependencies = append(dependencies, o.detectHyprlandTools()...) + } + + // Niri-specific tools + if wm == deps.WindowManagerNiri { + dependencies = append(dependencies, o.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, o.detectMatugen()) + dependencies = append(dependencies, o.detectDgop()) + + return dependencies, nil +} + +func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency { + return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { + cmd := exec.Command("rpm", "-q", pkg) + err := cmd.Run() + return err == nil +} + +func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency { + return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter")) +} + +func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + packages := map[string]PackageMapping{ + // Standard zypper packages + "git": {Name: "git", 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}, + + // DMS packages from OBS + "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": o.getQuickshellMapping(variants["quickshell"]), + "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + } + + switch wm { + case deps.WindowManagerHyprland: + packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} + packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} + packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} + case deps.WindowManagerNiri: + // Niri stable has native package support on openSUSE + niriVariant := variants["niri"] + packages["niri"] = o.getNiriMapping(niriVariant) + packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant) + } + + return packages +} + +func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"} + } + return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"} +} + +func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping { + if forceQuickshellGit || variant == deps.VariantGit { + return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} + } + return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} +} + +func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} + } + return PackageMapping{Name: "niri", Repository: RepoTypeSystem} +} + +func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} + } + return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} +} + +func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency { + status := deps.StatusMissing + if o.commandExists("xwayland-satellite") { + status = deps.StatusInstalled + } + + return deps.Dependency{ + Name: "xwayland-satellite", + Status: status, + Description: "Xwayland support", + Required: true, + } +} + +func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency { + status := deps.StatusMissing + if o.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 (o *OpenSUSEDistribution) getPrerequisites() []string { + return []string{} +} + +func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + prerequisites := o.getPrerequisites() + var missingPkgs []string + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Checking prerequisites...", + IsComplete: false, + LogOutput: "Checking prerequisite packages", + } + + for _, pkg := range prerequisites { + checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg) + if err := checkCmd.Run(); err != nil { + missingPkgs = append(missingPkgs, pkg) + } + } + + _, err := exec.LookPath("go") + if err != nil { + o.log("go not found in PATH, will install go") + missingPkgs = append(missingPkgs, "go") + } else { + o.log("go already available in PATH") + } + + if len(missingPkgs) == 0 { + o.log("All prerequisites already installed") + return nil + } + + o.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.08, + Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo zypper install -y %s", strings.Join(missingPkgs, " ")), + LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), + } + + args := []string{"zypper", "install", "-y"} + args = append(args, missingPkgs...) + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + output, err := cmd.CombinedOutput() + if err != nil { + o.logError("failed to install prerequisites", err) + o.log(fmt.Sprintf("Prerequisites command output: %s", string(output))) + return fmt.Errorf("failed to install prerequisites: %w", err) + } + o.log(fmt.Sprintf("Prerequisites install output: %s", string(output))) + + return nil +} + +func (o *OpenSUSEDistribution) 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 := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to disable install media repositories: %w", err) + } + + if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + // Enable OBS repositories + if len(obsPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.15, + Step: "Enabling OBS repositories...", + IsComplete: false, + LogOutput: "Setting up OBS repositories for additional packages", + } + if err := o.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to enable OBS repositories: %w", err) + } + } + + // Phase 3: System Packages (Zypper) + if len(systemPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), + } + if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil { + return fmt.Errorf("failed to install zypper packages: %w", err) + } + } + + // OBS Packages + obsPkgNames := o.extractPackageNames(obsPkgs) + if len(obsPkgNames) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, + Progress: 0.65, + Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)), + IsComplete: false, + LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), + } + if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil { + return fmt.Errorf("failed to install OBS packages: %w", err) + } + } + + // Manual Builds + if len(manualPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.85, + Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), + } + if err := o.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install manual packages: %w", err) + } + } + + // Configuration + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + terminal := o.DetectTerminalFromDeps(dependencies) + if err := o.WriteEnvironmentConfig(terminal); err != nil { + o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := o.WriteWindowManagerConfig(wm); err != nil { + o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := o.EnableDMSService(ctx, wm); err != nil { + o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + // Complete + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { + systemPkgs := []string{} + obsPkgs := []PackageMapping{} + manualPkgs := []string{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := o.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 { + o.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) + continue + } + + switch pkgInfo.Repository { + case RepoTypeSystem: + systemPkgs = append(systemPkgs, pkgInfo.Name) + case RepoTypeOBS: + obsPkgs = append(obsPkgs, pkgInfo) + case RepoTypeManual: + manualPkgs = append(manualPkgs, dep.Name) + } + } + + systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags)) + + return systemPkgs, obsPkgs, manualPkgs, variantMap +} + +func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string { + if wm != deps.WindowManagerNiri || disabledFlags["niri"] { + return nil + } + + return []string{openSUSENiriWaylandServerPackage} +} + +func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string { + for _, pkg := range extraPkgs { + if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) { + continue + } + + o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg)) + systemPkgs = append(systemPkgs, pkg) + } + + return systemPkgs +} + +func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string { + names := make([]string, len(packages)) + for i, pkg := range packages { + names[i] = pkg.Name + } + return names +} + +func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + enabledRepos := make(map[string]bool) + + osInfo, err := GetOSInfo() + if err != nil { + return fmt.Errorf("failed to get OS info: %w", err) + } + + obsDistroVersion := "openSUSE_Tumbleweed" + switch osInfo.Distribution.ID { + case "opensuse-leap": + obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID) + case "opensuse-slowroll": + obsDistroVersion = "openSUSE_Slowroll" + } + + for _, pkg := range obsPkgs { + if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] { + o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL)) + + // RepoURL format: "home:AvengeMedia:danklinux" + repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/") + repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-") + repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s/%s.repo", + repoPath, obsDistroVersion, pkg.RepoURL) + + checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName) + if checkCmd.Run() == nil { + o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL)) + enabledRepos[pkg.RepoURL] = true + continue + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.20, + Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL), + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("zypper addrepo -f %s", repoURL)) + if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { + o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err)) + } + + enabledRepos[pkg.RepoURL] = true + o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL)) + } + } + + // Refresh repositories with GPG auto-import + if len(enabledRepos) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.25, + Step: "Refreshing repositories...", + NeedsSudo: true, + CommandInfo: "sudo zypper --gpg-auto-import-keys refresh", + } + + refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh") + if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { + return fmt.Errorf("failed to refresh repositories: %w", err) + } + } + + return nil +} + +func isOpenSUSEInstallMediaURI(uri string) bool { + normalizedURI := strings.ToLower(strings.TrimSpace(uri)) + + return strings.HasPrefix(normalizedURI, "cd:/") || + strings.HasPrefix(normalizedURI, "dvd:/") || + strings.HasPrefix(normalizedURI, "hd:/") || + strings.HasPrefix(normalizedURI, "iso:/") +} + +func parseZypperInstallMediaAliases(output string) []string { + var aliases []string + + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" || !strings.Contains(line, "|") { + continue + } + + parts := strings.Split(line, "|") + if len(parts) < 7 { + continue + } + + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + + alias := parts[1] + enabled := strings.ToLower(parts[3]) + uri := parts[len(parts)-1] + + if alias == "" || strings.EqualFold(alias, "alias") { + continue + } + if enabled != "" && enabled != "yes" { + continue + } + if !isOpenSUSEInstallMediaURI(uri) { + continue + } + + aliases = append(aliases, alias) + } + + return aliases +} + +func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u") + output, err := listCmd.CombinedOutput() + if err != nil { + o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output)))) + return fmt.Errorf("failed to list zypper repositories: %w", err) + } + + aliases := parseZypperInstallMediaAliases(string(output)) + if len(aliases) == 0 { + return nil + } + + o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", "))) + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.055, + Step: "Disabling install media repositories...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")), + LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")), + } + + for _, alias := range aliases { + cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias))) + repoOutput, err := cmd.CombinedOutput() + if err != nil { + o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput)))) + return fmt.Errorf("failed to disable install media repo %s: %w", alias, err) + } + o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput)))) + } + + return nil +} + +func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string { + args := []string{"zypper", "install", "-y"} + if minimal { + args = append(args, "--no-recommends") + } + return append(args, packages...) +} + +func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { + if len(packages) == 0 { + return nil + } + + o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) + + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) + + groupIndex := 0 + installGroup := func(groupPackages []string, minimal bool) error { + if len(groupPackages) == 0 { + return nil + } + + groupIndex++ + groupStart := startProgress + groupEnd := endProgress + if totalGroups > 1 { + midpoint := startProgress + ((endProgress - startProgress) / 2) + if groupIndex == 1 { + groupEnd = midpoint + } else { + groupStart = midpoint + } + } + + args := o.zypperInstallArgs(groupPackages, minimal) + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: groupStart, + Step: step, + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) + } + + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil +} + +func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + o.log("Installing quickshell from source (with openSUSE-specific build flags)...") + + homeDir := os.Getenv("HOME") + if homeDir == "" { + return fmt.Errorf("HOME environment variable not set") + } + + cacheDir := filepath.Join(homeDir, ".cache", "dankinstall") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + tmpDir := filepath.Join(cacheDir, "quickshell-build") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.1, + Step: "Cloning quickshell repository...", + IsComplete: false, + CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git", + } + + var cloneCmd *exec.Cmd + if forceQuickshellGit || variant == deps.VariantGit { + cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir) + } else { + latestTag := o.getLatestQuickshellTag(ctx) + if latestTag != "" { + o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag)) + cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir) + } else { + o.log("Warning: failed to fetch latest tag, using default branch") + cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir) + } + } + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone quickshell: %w", err) + } + + buildDir := tmpDir + "/build" + if err := os.MkdirAll(buildDir, 0o755); err != nil { + return fmt.Errorf("failed to create build directory: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.3, + Step: "Configuring quickshell build (with openSUSE flags)...", + IsComplete: false, + CommandInfo: "cmake -B build -S . -G Ninja", + } + + // Get optflags from rpm + optflagsCmd := exec.CommandContext(ctx, "rpm", "--eval", "%{optflags}") + optflagsOutput, err := optflagsCmd.Output() + optflags := strings.TrimSpace(string(optflagsOutput)) + if err != nil || optflags == "" { + o.log("Warning: Could not get optflags from rpm, using default -O2 -g") + optflags = "-O2 -g" + } + + // Set openSUSE-specific CFLAGS + customCFLAGS := fmt.Sprintf("%s -I/usr/include/wayland", optflags) + + configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build", + "-DCMAKE_BUILD_TYPE=RelWithDebInfo", + "-DCRASH_REPORTER=off", + "-DCMAKE_CXX_STANDARD=20") + configureCmd.Dir = tmpDir + configureCmd.Env = append(os.Environ(), + "TMPDIR="+cacheDir, + "CFLAGS="+customCFLAGS, + "CXXFLAGS="+customCFLAGS) + + o.log(fmt.Sprintf("Using CFLAGS: %s", customCFLAGS)) + + output, err := configureCmd.CombinedOutput() + if err != nil { + o.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output))) + return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output)) + } + + o.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output))) + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.4, + Step: "Building quickshell (this may take a while)...", + IsComplete: false, + CommandInfo: "cmake --build build", + } + + buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build") + buildCmd.Dir = tmpDir + buildCmd.Env = append(os.Environ(), + "TMPDIR="+cacheDir, + "CFLAGS="+customCFLAGS, + "CXXFLAGS="+customCFLAGS) + if err := o.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil { + return fmt.Errorf("failed to build quickshell: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.8, + Step: "Installing quickshell...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo cmake --install build", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build") + installCmd.Dir = tmpDir + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install quickshell: %w", err) + } + + o.log("quickshell installed successfully from source") + return nil +} + +func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if o.commandExists("cargo") { + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.82, + Step: "Installing rustup...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo zypper install rustup", + } + + rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup") + if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { + return fmt.Errorf("failed to install rustup: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.83, + Step: "Installing stable Rust toolchain...", + IsComplete: false, + CommandInfo: "rustup install stable", + } + + rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable") + if err := o.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil { + return fmt.Errorf("failed to install Rust toolchain: %w", err) + } + + if !o.commandExists("cargo") { + o.log("Warning: cargo not found in PATH after Rust installation, trying to source environment") + } + + return nil +} + +func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", "))) + + for _, pkg := range packages { + if pkg == "matugen" { + if err := o.installRust(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install Rust: %w", err) + } + break + } + } + + for _, pkg := range packages { + variant := variantMap[pkg] + if pkg == "quickshell" { + if err := o.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install quickshell: %w", err) + } + } else { + if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install %s: %w", pkg, err) + } + } + } + + return nil +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/osinfo.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/osinfo.go new file mode 100644 index 0000000..8fc4b25 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/osinfo.go @@ -0,0 +1,122 @@ +package distros + +import ( + "bufio" + "fmt" + "os" + "runtime" + "strconv" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" +) + +// DistroInfo contains basic information about a distribution +type DistroInfo struct { + ID string + HexColorCode string +} + +// OSInfo contains complete OS information +type OSInfo struct { + Distribution DistroInfo + Version string + VersionID string + VersionCodename string + PrettyName string + Architecture string +} + +// GetOSInfo detects the current OS and returns information about it +func GetOSInfo() (*OSInfo, error) { + if runtime.GOOS != "linux" { + return nil, errdefs.NewCustomError(errdefs.ErrTypeNotLinux, fmt.Sprintf("Only linux is supported, but I found %s", runtime.GOOS)) + } + + if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + return nil, errdefs.NewCustomError(errdefs.ErrTypeInvalidArchitecture, fmt.Sprintf("Only amd64 and arm64 are supported, but I found %s", runtime.GOARCH)) + } + + info := &OSInfo{ + Architecture: runtime.GOARCH, + } + + file, err := os.Open("/etc/os-release") + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + value := strings.Trim(parts[1], "\"") + + switch key { + case "ID": + config, exists := Registry[value] + if !exists { + return nil, errdefs.NewCustomError(errdefs.ErrTypeUnsupportedDistribution, fmt.Sprintf("Unsupported distribution: %s", value)) + } + + info.Distribution = DistroInfo{ + ID: value, // Use the actual ID from os-release + HexColorCode: config.ColorHex, + } + case "VERSION_ID", "BUILD_ID": + info.VersionID = value + case "VERSION": + info.Version = value + case "VERSION_CODENAME": + info.VersionCodename = value + case "PRETTY_NAME": + info.PrettyName = value + } + } + + return info, scanner.Err() +} + +// IsUnsupportedDistro checks if a distribution/version combination is supported +func IsUnsupportedDistro(distroID, versionID string) bool { + if !IsDistroSupported(distroID) { + return true + } + + if distroID == "ubuntu" { + parts := strings.Split(versionID, ".") + if len(parts) >= 2 { + major, err1 := strconv.Atoi(parts[0]) + minor, err2 := strconv.Atoi(parts[1]) + + if err1 == nil && err2 == nil { + return major < 25 || (major == 25 && minor < 4) + } + } + return true + } + + if distroID == "debian" { + // unstable/sid support + if versionID == "sid" { + return false + } + if versionID == "" { + // debian testing/sid have no version ID + return false + } + versionNum, err := strconv.Atoi(versionID) + if err == nil { + return versionNum < 12 + } + return true + } + + return false +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/ubuntu.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/ubuntu.go new file mode 100644 index 0000000..37911f0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/ubuntu.go @@ -0,0 +1,702 @@ +package distros + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" +) + +func init() { + Register("ubuntu", "#E95420", FamilyUbuntu, func(config DistroConfig, logChan chan<- string) Distribution { + return NewUbuntuDistribution(config, logChan) + }) +} + +type UbuntuDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig +} + +func NewUbuntuDistribution(config DistroConfig, logChan chan<- string) *UbuntuDistribution { + base := NewBaseDistribution(logChan) + return &UbuntuDistribution{ + BaseDistribution: base, + ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, + config: config, + } +} + +func (u *UbuntuDistribution) GetID() string { + return u.config.ID +} + +func (u *UbuntuDistribution) GetColorHex() string { + return u.config.ColorHex +} + +func (u *UbuntuDistribution) GetFamily() DistroFamily { + return u.config.Family +} + +func (u *UbuntuDistribution) GetPackageManager() PackageManagerType { + return PackageManagerAPT +} + +func (u *UbuntuDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { + return u.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) +} + +func (u *UbuntuDistribution) 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, u.detectDMS()) + + // Terminal with choice support + dependencies = append(dependencies, u.detectSpecificTerminal(terminal)) + + // Common detections using base methods + dependencies = append(dependencies, u.detectGit()) + dependencies = append(dependencies, u.detectWindowManager(wm)) + dependencies = append(dependencies, u.detectQuickshell()) + dependencies = append(dependencies, u.detectDMSGreeter()) + dependencies = append(dependencies, u.detectXDGPortal()) + dependencies = append(dependencies, u.detectAccountsService()) + + // Hyprland-specific tools + if wm == deps.WindowManagerHyprland { + dependencies = append(dependencies, u.detectHyprlandTools()...) + } + + // Niri-specific tools + if wm == deps.WindowManagerNiri { + dependencies = append(dependencies, u.detectXwaylandSatellite()) + } + + dependencies = append(dependencies, u.detectMatugen()) + dependencies = append(dependencies, u.detectDgop()) + + return dependencies, nil +} + +func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency { + return u.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", u.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency { + return u.detectCommand("xwayland-satellite", "Xwayland support") +} + +func (u *UbuntuDistribution) detectAccountsService() deps.Dependency { + return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice")) +} + +func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency { + return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter")) +} + +func (u *UbuntuDistribution) packageInstalled(pkg string) bool { + return debianPackageInstalledPrecisely(pkg) +} + +func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { + return u.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) +} + +func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { + packages := map[string]PackageMapping{ + // Standard APT packages + "git": {Name: "git", 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}, + + // DMS packages from PPAs + "dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": u.getQuickshellMapping(variants["quickshell"]), + "dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + "dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + "ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + } + + switch wm { + case deps.WindowManagerHyprland: + // Use the cppiber PPA for Hyprland + packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"} + packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"} + packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} + case deps.WindowManagerNiri: + niriVariant := variants["niri"] + packages["niri"] = u.getNiriMapping(niriVariant) + packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant) + } + + return packages +} + +func (u *UbuntuDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms-git"} + } + return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms"} +} + +func (u *UbuntuDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping { + if forceQuickshellGit || variant == deps.VariantGit { + return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"} + } + return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"} +} + +func (u *UbuntuDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"} + } + return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"} +} + +func (u *UbuntuDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping { + if variant == deps.VariantGit { + return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"} + } + return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"} +} + +func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.06, + Step: "Updating package lists...", + IsComplete: false, + LogOutput: "Updating APT package lists", + } + + updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") + if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil { + return fmt.Errorf("failed to update package lists: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.08, + Step: "Installing build-essential...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install -y build-essential", + LogOutput: "Installing build tools", + } + + checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") + if err := checkCmd.Run(); err != nil { + // Not installed, install it + cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential") + if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { + return fmt.Errorf("failed to install build-essential: %w", err) + } + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.10, + Step: "Installing development dependencies...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev", + LogOutput: "Installing additional development tools", + } + + devToolsCmd := ExecSudoCommand(ctx, sudoPassword, + "apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev") + if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { + return fmt.Errorf("failed to install development tools: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.12, + Step: "Prerequisites installation complete", + IsComplete: false, + LogOutput: "Prerequisites successfully installed", + } + + return nil +} + +func (u *UbuntuDistribution) 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 := u.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install prerequisites: %w", err) + } + + systemPkgs, ppaPkgs, manualPkgs, variantMap := u.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + + // Phase 2: Enable PPA repositories + if len(ppaPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.15, + Step: "Enabling PPA repositories...", + IsComplete: false, + LogOutput: "Setting up PPA repositories for additional packages", + } + if err := u.enablePPARepos(ctx, ppaPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to enable PPA repositories: %w", err) + } + } + + // Phase 3: System Packages (APT) + if len(systemPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.35, + Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)), + IsComplete: false, + NeedsSudo: true, + LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), + } + if err := u.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install APT packages: %w", err) + } + } + + // Phase 4: PPA Packages + ppaPkgNames := u.extractPackageNames(ppaPkgs) + if len(ppaPkgNames) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseAURPackages, // Reusing AUR phase for PPA + Progress: 0.65, + Step: fmt.Sprintf("Installing %d PPA packages...", len(ppaPkgNames)), + IsComplete: false, + LogOutput: fmt.Sprintf("Installing PPA packages: %s", strings.Join(ppaPkgNames, ", ")), + } + if err := u.installPPAPackages(ctx, ppaPkgNames, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install PPA packages: %w", err) + } + } + + // Phase 5: Manual Builds + if len(manualPkgs) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.80, + Step: "Installing build dependencies...", + IsComplete: false, + LogOutput: "Installing build tools for manual compilation", + } + if err := u.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install build dependencies: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.85, + Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), + IsComplete: false, + LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), + } + if err := u.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install manual packages: %w", err) + } + } + + // Phase 6: Configuration + progressChan <- InstallProgressMsg{ + Phase: PhaseConfiguration, + Progress: 0.90, + Step: "Configuring system...", + IsComplete: false, + LogOutput: "Starting post-installation configuration...", + } + + terminal := u.DetectTerminalFromDeps(dependencies) + if err := u.WriteEnvironmentConfig(terminal); err != nil { + u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := u.WriteWindowManagerConfig(wm); err != nil { + u.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := u.EnableDMSService(ctx, wm); err != nil { + u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + // Phase 7: Complete + progressChan <- InstallProgressMsg{ + Phase: PhaseComplete, + Progress: 1.0, + Step: "Installation complete!", + IsComplete: true, + LogOutput: "All packages installed and configured successfully", + } + + return nil +} + +func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { + systemPkgs := []string{} + ppaPkgs := []PackageMapping{} + manualPkgs := []string{} + + variantMap := make(map[string]deps.PackageVariant) + for _, dep := range dependencies { + variantMap[dep.Name] = dep.Variant + } + + packageMap := u.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 { + u.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) + continue + } + + switch pkgInfo.Repository { + case RepoTypeSystem: + systemPkgs = append(systemPkgs, pkgInfo.Name) + case RepoTypePPA: + ppaPkgs = append(ppaPkgs, pkgInfo) + case RepoTypeManual: + manualPkgs = append(manualPkgs, dep.Name) + } + } + + return systemPkgs, ppaPkgs, manualPkgs, variantMap +} + +func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string { + names := make([]string, len(packages)) + for i, pkg := range packages { + names[i] = pkg.Name + } + return names +} + +func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + enabledRepos := make(map[string]bool) + + installPPACmd := ExecSudoCommand(ctx, sudoPassword, + "apt-get install -y software-properties-common") + if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil { + return fmt.Errorf("failed to install software-properties-common: %w", err) + } + + for _, pkg := range ppaPkgs { + if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] { + u.log(fmt.Sprintf("Enabling PPA repository: %s", pkg.RepoURL)) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.20, + Step: fmt.Sprintf("Enabling PPA repo %s...", pkg.RepoURL), + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, + fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL)) + if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { + u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err) + return fmt.Errorf("failed to enable PPA repo %s: %w", pkg.RepoURL, err) + } + u.log(fmt.Sprintf("PPA repo %s enabled successfully", pkg.RepoURL)) + enabledRepos[pkg.RepoURL] = true + } + } + + if len(enabledRepos) > 0 { + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.25, + Step: "Updating package lists...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get update", + } + + updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") + if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { + return fmt.Errorf("failed to update package lists after adding PPAs: %w", err) + } + } + + return nil +} + +func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) + return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60) +} + +func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", "))) + return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85) +} + +func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string { + args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} + if minimal { + args = append(args, "--no-install-recommends") + } + return append(args, packages...) +} + +func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) + + groupIndex := 0 + installGroup := func(groupPackages []string, minimal bool) error { + if len(groupPackages) == 0 { + return nil + } + + groupIndex++ + groupStart := startProgress + groupEnd := endProgress + if totalGroups > 1 { + midpoint := startProgress + ((endProgress - startProgress) / 2) + if groupIndex == 1 { + groupEnd = midpoint + } else { + groupStart = midpoint + } + } + + args := u.aptInstallArgs(groupPackages, minimal) + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: groupStart, + Step: step, + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) + } + + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil +} + +func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + buildDeps := make(map[string]bool) + + for _, pkg := range manualPkgs { + switch pkg { + case "niri": + buildDeps["curl"] = true + buildDeps["libxkbcommon-dev"] = true + buildDeps["libwayland-dev"] = true + buildDeps["libudev-dev"] = true + buildDeps["libinput-dev"] = true + buildDeps["libdisplay-info-dev"] = true + buildDeps["libpango1.0-dev"] = true + buildDeps["libcairo-dev"] = true + buildDeps["libpipewire-0.3-dev"] = true + buildDeps["libc6-dev"] = true + buildDeps["clang"] = true + buildDeps["libseat-dev"] = true + buildDeps["libgbm-dev"] = true + buildDeps["alacritty"] = true + buildDeps["fuzzel"] = true + buildDeps["libxcb-cursor-dev"] = true + case "quickshell": + buildDeps["qt6-base-dev"] = true + buildDeps["qt6-base-private-dev"] = true + buildDeps["qt6-declarative-dev"] = true + buildDeps["qt6-declarative-private-dev"] = true + buildDeps["qt6-wayland-dev"] = true + buildDeps["qt6-wayland-private-dev"] = true + buildDeps["qt6-tools-dev"] = true + buildDeps["libqt6svg6-dev"] = true + buildDeps["qt6-shadertools-dev"] = true + buildDeps["spirv-tools"] = true + buildDeps["libcli11-dev"] = true + buildDeps["libjemalloc-dev"] = true + buildDeps["libwayland-dev"] = true + buildDeps["wayland-protocols"] = true + buildDeps["libdrm-dev"] = true + buildDeps["libgbm-dev"] = true + buildDeps["libegl-dev"] = true + buildDeps["libgles2-mesa-dev"] = true + buildDeps["libgl1-mesa-dev"] = true + buildDeps["libxcb1-dev"] = true + buildDeps["libpipewire-0.3-dev"] = true + buildDeps["libpam0g-dev"] = true + case "matugen": + buildDeps["curl"] = true + } + } + + for _, pkg := range manualPkgs { + switch pkg { + case "niri", "matugen": + if err := u.installRust(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install Rust: %w", err) + } + case "dgop": + if err := u.installGo(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install Go: %w", err) + } + } + } + + if len(buildDeps) == 0 { + return nil + } + + depList := make([]string, 0, len(buildDeps)) + for dep := range buildDeps { + depList = append(depList, dep) + } + + args := []string{"apt-get", "install", "-y"} + args = append(args, depList...) + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82) +} + +func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if u.commandExists("cargo") { + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.82, + Step: "Installing rustup...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install rustup", + } + + rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup") + if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { + return fmt.Errorf("failed to install rustup: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.83, + Step: "Installing stable Rust toolchain...", + IsComplete: false, + CommandInfo: "rustup install stable", + } + + rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable") + if err := u.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil { + return fmt.Errorf("failed to install Rust toolchain: %w", err) + } + + // Verify cargo is now available + if !u.commandExists("cargo") { + u.log("Warning: cargo not found in PATH after Rust installation, trying to source environment") + } + + return nil +} + +func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if u.commandExists("go") { + return nil + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.87, + Step: "Adding Go PPA repository...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports", + } + + addPPACmd := ExecSudoCommand(ctx, sudoPassword, + "add-apt-repository -y ppa:longsleep/golang-backports") + if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil { + return fmt.Errorf("failed to add Go PPA: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.88, + Step: "Updating package lists...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get update", + } + + updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") + if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil { + return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err) + } + + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: 0.89, + Step: "Installing Go...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: "sudo apt-get install golang-go", + } + + installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go") + return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90) +} + +func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + if len(packages) == 0 { + return nil + } + + u.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", "))) + + for _, pkg := range packages { + switch pkg { + default: + if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to install %s: %w", pkg, err) + } + } + } + + return nil +} |