summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/arch.go807
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base.go721
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/base_test.go216
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/debian.go706
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/factory.go19
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/fedora.go550
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/gentoo.go700
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/interface.go158
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages.go663
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/manual_packages_test.go122
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/minimal_install.go44
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go856
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/osinfo.go122
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/ubuntu.go702
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
+}