package config import ( "context" "fmt" "os" "path/filepath" "regexp" "strings" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" ) type ConfigDeployer struct { logChan chan<- string } type DeploymentResult struct { ConfigType string Path string BackupPath string Deployed bool Error error } func NewConfigDeployer(logChan chan<- string) *ConfigDeployer { return &ConfigDeployer{ logChan: logChan, } } func (cd *ConfigDeployer) log(message string) { if cd.logChan != nil { cd.logChan <- message } } // DeployConfigurations deploys all necessary configurations based on the chosen window manager func (cd *ConfigDeployer) DeployConfigurations(ctx context.Context, wm deps.WindowManager) ([]DeploymentResult, error) { return cd.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty) } // DeployConfigurationsWithTerminal deploys all necessary configurations based on chosen window manager and terminal func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]DeploymentResult, error) { return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil) } func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) { return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil) } func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) { var results []DeploymentResult shouldReplaceConfig := func(configType string) bool { if replaceConfigs == nil { return true } replace, exists := replaceConfigs[configType] return !exists || replace } switch wm { case deps.WindowManagerNiri: if shouldReplaceConfig("Niri") { result, err := cd.deployNiriConfig(terminal) results = append(results, result) if err != nil { return results, fmt.Errorf("failed to deploy Niri config: %w", err) } } case deps.WindowManagerHyprland: if shouldReplaceConfig("Hyprland") { result, err := cd.deployHyprlandConfig(terminal) results = append(results, result) if err != nil { return results, fmt.Errorf("failed to deploy Hyprland config: %w", err) } } } switch terminal { case deps.TerminalGhostty: if shouldReplaceConfig("Ghostty") { ghosttyResults, err := cd.deployGhosttyConfig() results = append(results, ghosttyResults...) if err != nil { return results, fmt.Errorf("failed to deploy Ghostty config: %w", err) } } case deps.TerminalKitty: if shouldReplaceConfig("Kitty") { kittyResults, err := cd.deployKittyConfig() results = append(results, kittyResults...) if err != nil { return results, fmt.Errorf("failed to deploy Kitty config: %w", err) } } case deps.TerminalAlacritty: if shouldReplaceConfig("Alacritty") { alacrittyResults, err := cd.deployAlacrittyConfig() results = append(results, alacrittyResults...) if err != nil { return results, fmt.Errorf("failed to deploy Alacritty config: %w", err) } } } return results, nil } // deployNiriConfig handles Niri configuration deployment with backup and merging func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) { result := DeploymentResult{ ConfigType: "Niri", Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"), } configDir := filepath.Dir(result.Path) if err := os.MkdirAll(configDir, 0755); err != nil { result.Error = fmt.Errorf("failed to create config directory: %w", err) return result, result.Error } var existingConfig string if _, err := os.Stat(result.Path); err == nil { cd.log("Found existing Niri configuration") existingData, err := os.ReadFile(result.Path) if err != nil { result.Error = fmt.Errorf("failed to read existing config: %w", err) return result, result.Error } existingConfig = string(existingData) timestamp := time.Now().Format("2006-01-02_15-04-05") result.BackupPath = result.Path + ".backup." + timestamp if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil { result.Error = fmt.Errorf("failed to create backup: %w", err) return result, result.Error } cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) } // Detect polkit agent path polkitPath, err := cd.detectPolkitAgent() if err != nil { cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err)) polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback } // Determine terminal command based on choice var terminalCommand string switch terminal { case deps.TerminalGhostty: terminalCommand = "ghostty" case deps.TerminalKitty: terminalCommand = "kitty" case deps.TerminalAlacritty: terminalCommand = "alacritty" default: terminalCommand = "ghostty" // fallback to ghostty } newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand) // If there was an existing config, merge the output sections if existingConfig != "" { mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) if err != nil { cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err)) } else { newConfig = mergedConfig cd.log("Successfully merged existing output sections") } } if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil { result.Error = fmt.Errorf("failed to write config: %w", err) return result, result.Error } result.Deployed = true cd.log("Successfully deployed Niri configuration") return result, nil } func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) { var results []DeploymentResult mainResult := DeploymentResult{ ConfigType: "Ghostty", Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"), } configDir := filepath.Dir(mainResult.Path) if err := os.MkdirAll(configDir, 0755); err != nil { mainResult.Error = fmt.Errorf("failed to create config directory: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } if _, err := os.Stat(mainResult.Path); err == nil { cd.log("Found existing Ghostty configuration") existingData, err := os.ReadFile(mainResult.Path) if err != nil { mainResult.Error = fmt.Errorf("failed to read existing config: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } timestamp := time.Now().Format("2006-01-02_15-04-05") mainResult.BackupPath = mainResult.Path + ".backup." + timestamp if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil { mainResult.Error = fmt.Errorf("failed to create backup: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath)) } if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil { mainResult.Error = fmt.Errorf("failed to write config: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } mainResult.Deployed = true cd.log("Successfully deployed Ghostty configuration") results = append(results, mainResult) colorResult := DeploymentResult{ ConfigType: "Ghostty Colors", Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"), } if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil { colorResult.Error = fmt.Errorf("failed to write color config: %w", err) return results, colorResult.Error } colorResult.Deployed = true cd.log("Successfully deployed Ghostty color configuration") results = append(results, colorResult) return results, nil } func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) { var results []DeploymentResult mainResult := DeploymentResult{ ConfigType: "Kitty", Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"), } configDir := filepath.Dir(mainResult.Path) if err := os.MkdirAll(configDir, 0755); err != nil { mainResult.Error = fmt.Errorf("failed to create config directory: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } if _, err := os.Stat(mainResult.Path); err == nil { cd.log("Found existing Kitty configuration") existingData, err := os.ReadFile(mainResult.Path) if err != nil { mainResult.Error = fmt.Errorf("failed to read existing config: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } timestamp := time.Now().Format("2006-01-02_15-04-05") mainResult.BackupPath = mainResult.Path + ".backup." + timestamp if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil { mainResult.Error = fmt.Errorf("failed to create backup: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath)) } if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil { mainResult.Error = fmt.Errorf("failed to write config: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } mainResult.Deployed = true cd.log("Successfully deployed Kitty configuration") results = append(results, mainResult) themeResult := DeploymentResult{ ConfigType: "Kitty Theme", Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"), } if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil { themeResult.Error = fmt.Errorf("failed to write theme config: %w", err) return results, themeResult.Error } themeResult.Deployed = true cd.log("Successfully deployed Kitty theme configuration") results = append(results, themeResult) tabsResult := DeploymentResult{ ConfigType: "Kitty Tabs", Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"), } if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil { tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err) return results, tabsResult.Error } tabsResult.Deployed = true cd.log("Successfully deployed Kitty tabs configuration") results = append(results, tabsResult) return results, nil } func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) { var results []DeploymentResult mainResult := DeploymentResult{ ConfigType: "Alacritty", Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"), } configDir := filepath.Dir(mainResult.Path) if err := os.MkdirAll(configDir, 0755); err != nil { mainResult.Error = fmt.Errorf("failed to create config directory: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } if _, err := os.Stat(mainResult.Path); err == nil { cd.log("Found existing Alacritty configuration") existingData, err := os.ReadFile(mainResult.Path) if err != nil { mainResult.Error = fmt.Errorf("failed to read existing config: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } timestamp := time.Now().Format("2006-01-02_15-04-05") mainResult.BackupPath = mainResult.Path + ".backup." + timestamp if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil { mainResult.Error = fmt.Errorf("failed to create backup: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath)) } if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil { mainResult.Error = fmt.Errorf("failed to write config: %w", err) return []DeploymentResult{mainResult}, mainResult.Error } mainResult.Deployed = true cd.log("Successfully deployed Alacritty configuration") results = append(results, mainResult) themeResult := DeploymentResult{ ConfigType: "Alacritty Theme", Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"), } if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil { themeResult.Error = fmt.Errorf("failed to write theme config: %w", err) return results, themeResult.Error } themeResult.Deployed = true cd.log("Successfully deployed Alacritty theme configuration") results = append(results, themeResult) return results, nil } // detectPolkitAgent tries to find the polkit authentication agent on the system // Prioritizes mate-polkit paths since that's what we install func (cd *ConfigDeployer) detectPolkitAgent() (string, error) { // Prioritize mate-polkit paths first matePaths := []string{ "/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1", "/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1", "/usr/lib/polkit-mate/polkit-mate-authentication-agent-1", "/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1", } for _, path := range matePaths { if _, err := os.Stat(path); err == nil { cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path)) return path, nil } } // Fallback to other polkit agents if mate-polkit is not found fallbackPaths := []string{ "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1", "/usr/libexec/polkit-gnome-authentication-agent-1", } for _, path := range fallbackPaths { if _, err := os.Stat(path); err == nil { cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path)) return path, nil } } return "", fmt.Errorf("no polkit agent found in common locations") } // mergeNiriOutputSections extracts output sections from existing config and merges them into the new config func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) { // Regular expression to match output sections (including commented ones) outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`) // Find all output sections in the existing config existingOutputs := outputRegex.FindAllString(existingConfig, -1) if len(existingOutputs) == 0 { // No output sections to merge return newConfig, nil } // Remove the example output section from the new config exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`) mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "") // Find where to insert the output sections (after the input section) inputEndRegex := regexp.MustCompile(`(?m)^}$`) inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1) if len(inputMatches) < 1 { return "", fmt.Errorf("could not find insertion point for output sections") } // Insert after the first closing brace (end of input section) insertPos := inputMatches[0][1] var builder strings.Builder builder.WriteString(mergedConfig[:insertPos]) builder.WriteString("\n// Outputs from existing configuration\n") for _, output := range existingOutputs { builder.WriteString(output) builder.WriteString("\n") } builder.WriteString(mergedConfig[insertPos:]) return builder.String(), nil } // deployHyprlandConfig handles Hyprland configuration deployment with backup and merging func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) { result := DeploymentResult{ ConfigType: "Hyprland", Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"), } configDir := filepath.Dir(result.Path) if err := os.MkdirAll(configDir, 0755); err != nil { result.Error = fmt.Errorf("failed to create config directory: %w", err) return result, result.Error } var existingConfig string if _, err := os.Stat(result.Path); err == nil { cd.log("Found existing Hyprland configuration") existingData, err := os.ReadFile(result.Path) if err != nil { result.Error = fmt.Errorf("failed to read existing config: %w", err) return result, result.Error } existingConfig = string(existingData) timestamp := time.Now().Format("2006-01-02_15-04-05") result.BackupPath = result.Path + ".backup." + timestamp if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil { result.Error = fmt.Errorf("failed to create backup: %w", err) return result, result.Error } cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) } // Detect polkit agent path polkitPath, err := cd.detectPolkitAgent() if err != nil { cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err)) polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback } // Determine terminal command based on choice var terminalCommand string switch terminal { case deps.TerminalGhostty: terminalCommand = "ghostty" case deps.TerminalKitty: terminalCommand = "kitty" case deps.TerminalAlacritty: terminalCommand = "alacritty" default: terminalCommand = "ghostty" // fallback to ghostty } newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand) // If there was an existing config, merge the monitor sections if existingConfig != "" { mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) if err != nil { cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err)) } else { newConfig = mergedConfig cd.log("Successfully merged existing monitor sections") } } if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil { result.Error = fmt.Errorf("failed to write config: %w", err) return result, result.Error } result.Deployed = true cd.log("Successfully deployed Hyprland configuration") return result, nil } // mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) { // Regular expression to match monitor lines (including commented ones) // Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc. // Also matches commented versions: # monitor = ... monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`) // Find all monitor lines in the existing config existingMonitors := monitorRegex.FindAllString(existingConfig, -1) if len(existingMonitors) == 0 { // No monitor sections to merge return newConfig, nil } // Remove the example monitor line from the new config exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`) mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "") // Find where to insert the monitor sections (after the MONITOR CONFIG header) monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`) headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig) if headerMatch == nil { return "", fmt.Errorf("could not find MONITOR CONFIG section") } // Insert after the header insertPos := headerMatch[1] + 1 // +1 for the newline var builder strings.Builder builder.WriteString(mergedConfig[:insertPos]) builder.WriteString("# Monitors from existing configuration\n") for _, monitor := range existingMonitors { builder.WriteString(monitor) builder.WriteString("\n") } builder.WriteString(mergedConfig[insertPos:]) return builder.String(), nil }