diff --git a/core/internal/distros/debian.go b/core/internal/distros/debian.go index 3c9aa5c1..99d66055 100644 --- a/core/internal/distros/debian.go +++ b/core/internal/distros/debian.go @@ -70,7 +70,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context, dependencies = append(dependencies, d.detectMatugen()) dependencies = append(dependencies, d.detectDgop()) - dependencies = append(dependencies, d.detectHyprpicker()) dependencies = append(dependencies, d.detectClipboardTools()...) return dependencies, nil @@ -139,7 +138,12 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool { } 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}, @@ -148,24 +152,54 @@ func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string "mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, - "dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}, - "niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}, - "quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"}, - "ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"}, - "matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"}, - "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, - "cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"}, - "hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"}, + // DMS packages from OBS with variant support + "dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": d.getQuickshellMapping(variants["quickshell"]), + "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + + // Keep ghostty as manual (no OBS package yet) + "ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"}, } if wm == deps.WindowManagerNiri { - packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"} - packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"} + 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, @@ -238,8 +272,23 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [ return fmt.Errorf("failed to install prerequisites: %w", err) } - systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + 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, @@ -254,6 +303,22 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [ } } + // 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, @@ -297,8 +362,9 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [ return nil } -func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) { +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) @@ -306,7 +372,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, variantMap[dep.Name] = dep.Variant } - packageMap := d.GetPackageMapping(wm) + packageMap := d.GetPackageMappingWithVariants(wm, variantMap) for _, dep := range dependencies { if disabledFlags[dep.Name] { @@ -326,12 +392,116 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, 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, manualPkgs, variantMap + 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) 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" + } + + 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("curl -fsSL %s/Release.key | gpg --dearmor -o %s", 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] %s/ /", keyringPath, 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("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 { diff --git a/core/internal/distros/interface.go b/core/internal/distros/interface.go index 70a86873..8d9e66f1 100644 --- a/core/internal/distros/interface.go +++ b/core/internal/distros/interface.go @@ -39,6 +39,7 @@ const ( 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 diff --git a/core/internal/distros/opensuse.go b/core/internal/distros/opensuse.go index 11c1bc94..43fcf31d 100644 --- a/core/internal/distros/opensuse.go +++ b/core/internal/distros/opensuse.go @@ -82,7 +82,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex // Base detections (common across distros) dependencies = append(dependencies, o.detectMatugen()) dependencies = append(dependencies, o.detectDgop()) - dependencies = append(dependencies, o.detectHyprpicker()) dependencies = append(dependencies, o.detectClipboardTools()...) return dependencies, nil @@ -138,13 +137,12 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag "mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "cliphist": {Name: "cliphist", Repository: RepoTypeSystem}, - "hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem}, - // Manual builds - "dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}, - "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, - "quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"}, - "matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"}, + // DMS packages from OBS + "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": o.getQuickshellMapping(variants["quickshell"]), + "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, } switch wm { @@ -156,13 +154,43 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} case deps.WindowManagerNiri: - packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem} - packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} + // 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") { @@ -294,9 +322,23 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies return fmt.Errorf("failed to install prerequisites: %w", err) } - systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) + systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) - // Phase 2: System Packages (Zypper) + // 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, @@ -311,7 +353,22 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies } } - // Phase 3: Manual Builds + // 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); err != nil { + return fmt.Errorf("failed to install OBS packages: %w", err) + } + } + + // Manual Builds if len(manualPkgs) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, @@ -325,7 +382,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies } } - // Phase 4: Configuration + // Configuration progressChan <- InstallProgressMsg{ Phase: PhaseConfiguration, Progress: 0.90, @@ -334,7 +391,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies LogOutput: "Starting post-installation configuration...", } - // Phase 5: Complete + // Complete progressChan <- InstallProgressMsg{ Phase: PhaseComplete, Progress: 1.0, @@ -346,8 +403,9 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies return nil } -func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) { +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) @@ -375,12 +433,80 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency 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, manualPkgs, variantMap + return systemPkgs, obsPkgs, manualPkgs, variantMap +} + +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) + + 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/openSUSE_Tumbleweed/%s.repo", + repoPath, 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 { + return fmt.Errorf("failed to enable OBS repo %s: %w", 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 (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { diff --git a/core/internal/distros/ubuntu.go b/core/internal/distros/ubuntu.go index 41daacb0..d254aa6e 100644 --- a/core/internal/distros/ubuntu.go +++ b/core/internal/distros/ubuntu.go @@ -82,7 +82,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context, // Base detections (common across distros) dependencies = append(dependencies, u.detectMatugen()) dependencies = append(dependencies, u.detectDgop()) - dependencies = append(dependencies, u.detectHyprpicker()) dependencies = append(dependencies, u.detectClipboardTools()...) return dependencies, nil @@ -151,6 +150,10 @@ func (u *UbuntuDistribution) packageInstalled(pkg string) bool { } 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}, @@ -160,16 +163,16 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, - "hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}, - // Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs) - "dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}, - "niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}, - "quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"}, - "ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"}, - "matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"}, - "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, - "cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"}, + // DMS packages from PPAs + "dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": u.getQuickshellMapping(variants["quickshell"]), + "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + "dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + "cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, + + // Keep ghostty as manual (no PPA available) + "ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"}, } switch wm { @@ -182,13 +185,42 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} case deps.WindowManagerNiri: - packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"} - packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"} + 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, @@ -365,7 +397,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, variantMap[dep.Name] = dep.Variant } - packageMap := u.GetPackageMapping(wm) + packageMap := u.GetPackageMappingWithVariants(wm, variantMap) for _, dep := range dependencies { if disabledFlags[dep.Name] {