summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/config/deployer.go
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/config/deployer.go')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/config/deployer.go718
1 files changed, 718 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/config/deployer.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/config/deployer.go
new file mode 100644
index 0000000..7cc513f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/config/deployer.go
@@ -0,0 +1,718 @@
+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)
+}
+
+// DeployConfigurationsWithSystemd deploys configurations with systemd option
+func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
+ return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
+}
+
+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) {
+ return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
+}
+
+func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
+ var results []DeploymentResult
+
+ // Primary config file paths used to detect fresh installs.
+ configPrimaryPaths := map[string]string{
+ "Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
+ "Hyprland": filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
+ "Ghostty": filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
+ "Kitty": filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
+ "Alacritty": filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
+ }
+
+ shouldReplaceConfig := func(configType string) bool {
+ if replaceConfigs == nil {
+ return true
+ }
+ replace, exists := replaceConfigs[configType]
+ if !exists || replace {
+ return true
+ }
+ // Config is explicitly set to "don't replace" — but still deploy
+ // if the config file doesn't exist yet (fresh install scenario).
+ if primaryPath, ok := configPrimaryPaths[configType]; ok {
+ if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
+ return true
+ }
+ }
+ return false
+ }
+
+ switch wm {
+ case deps.WindowManagerNiri:
+ if shouldReplaceConfig("Niri") {
+ result, err := cd.deployNiriConfig(terminal, useSystemd)
+ 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, useSystemd)
+ 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
+}
+
+func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (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, 0o755); err != nil {
+ result.Error = fmt.Errorf("failed to create config directory: %w", err)
+ return result, result.Error
+ }
+
+ dmsDir := filepath.Join(configDir, "dms")
+ if err := os.MkdirAll(dmsDir, 0o755); err != nil {
+ result.Error = fmt.Errorf("failed to create dms 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, 0o644); 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))
+ }
+
+ var terminalCommand string
+ switch terminal {
+ case deps.TerminalGhostty:
+ terminalCommand = "ghostty"
+ case deps.TerminalKitty:
+ terminalCommand = "kitty"
+ case deps.TerminalAlacritty:
+ terminalCommand = "alacritty"
+ default:
+ terminalCommand = "ghostty"
+ }
+
+ newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
+
+ if !useSystemd {
+ newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
+ }
+
+ if existingConfig != "" {
+ mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig, dmsDir)
+ 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), 0o644); err != nil {
+ result.Error = fmt.Errorf("failed to write config: %w", err)
+ return result, result.Error
+ }
+
+ if err := cd.deployNiriDmsConfigs(dmsDir, terminalCommand); err != nil {
+ result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
+ return result, result.Error
+ }
+
+ result.Deployed = true
+ cd.log("Successfully deployed Niri configuration")
+ return result, nil
+}
+
+func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) error {
+ configs := []struct {
+ name string
+ content string
+ }{
+ {"colors.kdl", NiriColorsConfig},
+ {"layout.kdl", NiriLayoutConfig},
+ {"alttab.kdl", NiriAlttabConfig},
+ {"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
+ {"outputs.kdl", ""},
+ {"cursor.kdl", ""},
+ {"windowrules.kdl", ""},
+ }
+
+ for _, cfg := range configs {
+ path := filepath.Join(dmsDir, cfg.name)
+ // Skip if file already exists and is not empty to preserve user modifications
+ if info, err := os.Stat(path); err == nil && info.Size() > 0 {
+ cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
+ continue
+ }
+ if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
+ return fmt.Errorf("failed to write %s: %w", cfg.name, err)
+ }
+ cd.log(fmt.Sprintf("Deployed %s", cfg.name))
+ }
+
+ return 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, 0o755); 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, 0o644); 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), 0o644); 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", "themes", "dankcolors"),
+ }
+
+ themesDir := filepath.Dir(colorResult.Path)
+ if err := os.MkdirAll(themesDir, 0o755); err != nil {
+ mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
+ return []DeploymentResult{mainResult}, mainResult.Error
+ }
+
+ if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0o644); 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, 0o755); 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, 0o644); 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), 0o644); 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), 0o644); 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), 0o644); 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, 0o755); 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, 0o644); 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), 0o644); 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), 0o644); 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
+}
+
+func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
+ outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
+ existingOutputs := outputRegex.FindAllString(existingConfig, -1)
+
+ if len(existingOutputs) == 0 {
+ return newConfig, nil
+ }
+
+ outputsPath := filepath.Join(dmsDir, "outputs.kdl")
+ if _, err := os.Stat(outputsPath); err != nil {
+ var outputsContent strings.Builder
+ for _, output := range existingOutputs {
+ outputsContent.WriteString(output)
+ outputsContent.WriteString("\n\n")
+ }
+ if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
+ cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
+ } else {
+ cd.log("Migrated output sections to dms/outputs.kdl")
+ }
+ }
+
+ exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
+ mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
+
+ inputEndRegex := regexp.MustCompile(`(?m)^}$`)
+ inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
+
+ if len(inputMatches) < 1 {
+ return "", fmt.Errorf("could not find insertion point for output sections")
+ }
+
+ 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, useSystemd bool) (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, 0o755); err != nil {
+ result.Error = fmt.Errorf("failed to create config directory: %w", err)
+ return result, result.Error
+ }
+
+ dmsDir := filepath.Join(configDir, "dms")
+ if err := os.MkdirAll(dmsDir, 0o755); err != nil {
+ result.Error = fmt.Errorf("failed to create dms 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, 0o644); 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))
+ }
+
+ var terminalCommand string
+ switch terminal {
+ case deps.TerminalGhostty:
+ terminalCommand = "ghostty"
+ case deps.TerminalKitty:
+ terminalCommand = "kitty"
+ case deps.TerminalAlacritty:
+ terminalCommand = "alacritty"
+ default:
+ terminalCommand = "ghostty"
+ }
+
+ newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
+
+ if !useSystemd {
+ newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
+ }
+
+ if existingConfig != "" {
+ mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir)
+ 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), 0o644); err != nil {
+ result.Error = fmt.Errorf("failed to write config: %w", err)
+ return result, result.Error
+ }
+
+ if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
+ result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
+ return result, result.Error
+ }
+
+ result.Deployed = true
+ cd.log("Successfully deployed Hyprland configuration")
+ return result, nil
+}
+
+func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
+ configs := []struct {
+ name string
+ content string
+ }{
+ {"colors.conf", HyprColorsConfig},
+ {"layout.conf", HyprLayoutConfig},
+ {"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
+ {"outputs.conf", ""},
+ {"cursor.conf", ""},
+ {"windowrules.conf", ""},
+ }
+
+ for _, cfg := range configs {
+ path := filepath.Join(dmsDir, cfg.name)
+ // Skip if file already exists and is not empty to preserve user modifications
+ if info, err := os.Stat(path); err == nil && info.Size() > 0 {
+ cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
+ continue
+ }
+ if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
+ return fmt.Errorf("failed to write %s: %w", cfg.name, err)
+ }
+ cd.log(fmt.Sprintf("Deployed %s", cfg.name))
+ }
+
+ return nil
+}
+
+func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
+ monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
+ existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
+
+ if len(existingMonitors) == 0 {
+ return newConfig, nil
+ }
+
+ outputsPath := filepath.Join(dmsDir, "outputs.conf")
+ if _, err := os.Stat(outputsPath); err != nil {
+ var outputsContent strings.Builder
+ for _, monitor := range existingMonitors {
+ outputsContent.WriteString(monitor)
+ outputsContent.WriteString("\n")
+ }
+ if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
+ cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
+ } else {
+ cd.log("Migrated monitor sections to dms/outputs.conf")
+ }
+ }
+
+ exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
+ mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
+
+ monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
+ headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
+
+ if headerMatch == nil {
+ return "", fmt.Errorf("could not find MONITOR CONFIG section")
+ }
+
+ insertPos := headerMatch[1] + 1
+
+ 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
+}
+
+func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
+ lines := strings.Split(config, "\n")
+ var result []string
+ startupSectionFound := false
+
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
+ continue
+ }
+ if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
+ startupSectionFound = true
+ result = append(result, "exec-once = dms run")
+ result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
+ result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
+ result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
+ result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
+ result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
+ continue
+ }
+ result = append(result, line)
+ }
+
+ if !startupSectionFound {
+ for i, line := range result {
+ if strings.Contains(line, "STARTUP APPS") {
+ insertLines := []string{
+ "exec-once = dms run",
+ "env = QT_QPA_PLATFORM,wayland;xcb",
+ "env = ELECTRON_OZONE_PLATFORM_HINT,auto",
+ "env = QT_QPA_PLATFORMTHEME,gtk3",
+ "env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
+ fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
+ }
+ result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
+ break
+ }
+ }
+ }
+
+ return strings.Join(result, "\n")
+}
+
+func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
+ envVars := fmt.Sprintf(`environment {
+ XDG_CURRENT_DESKTOP "niri"
+ QT_QPA_PLATFORM "wayland;xcb"
+ ELECTRON_OZONE_PLATFORM_HINT "auto"
+ QT_QPA_PLATFORMTHEME "gtk3"
+ QT_QPA_PLATFORMTHEME_QT6 "gtk3"
+ TERMINAL "%s"
+}`, terminalCommand)
+
+ config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
+
+ spawnDms := `spawn-at-startup "dms" "run"`
+ if !strings.Contains(config, spawnDms) {
+ // Insert spawn-at-startup for dms after the environment block
+ envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
+ if loc := envBlockEnd.FindStringIndex(config); loc != nil {
+ config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
+ }
+ }
+
+ return config
+}