diff --git a/core/cmd/dms/commands_setup.go b/core/cmd/dms/commands_setup.go index e6281dfe..acde3f2a 100644 --- a/core/cmd/dms/commands_setup.go +++ b/core/cmd/dms/commands_setup.go @@ -29,6 +29,7 @@ func runSetup() error { wm, wmSelected := promptCompositor() terminal, terminalSelected := promptTerminal() + useSystemd := promptSystemd() if !wmSelected && !terminalSelected { fmt.Println("No configurations selected. Exiting.") @@ -67,14 +68,14 @@ func runSetup() error { var err error if wmSelected && terminalSelected { - results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal) + results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd) } else if wmSelected { - results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty) + results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd) if len(results) > 1 { results = results[:1] } } else if terminalSelected { - results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal) + results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd) if len(results) > 0 && results[0].ConfigType == "Niri" { results = results[1:] } @@ -144,6 +145,19 @@ func promptTerminal() (deps.Terminal, bool) { } } +func promptSystemd() bool { + fmt.Println("\nUse systemd for session management?") + fmt.Println("1) Yes (recommended for most distros)") + fmt.Println("2) No (standalone, no systemd integration)") + + var response string + fmt.Print("\nChoice (1-2): ") + fmt.Scanln(&response) + response = strings.TrimSpace(response) + + return response != "2" +} + func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool { homeDir := os.Getenv("HOME") willBackup := false diff --git a/core/internal/config/deployer.go b/core/internal/config/deployer.go index 2c2ef30a..56946055 100644 --- a/core/internal/config/deployer.go +++ b/core/internal/config/deployer.go @@ -46,11 +46,20 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, 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 shouldReplaceConfig := func(configType string) bool { @@ -64,7 +73,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex switch wm { case deps.WindowManagerNiri: if shouldReplaceConfig("Niri") { - result, err := cd.deployNiriConfig(terminal) + result, err := cd.deployNiriConfig(terminal, useSystemd) results = append(results, result) if err != nil { return results, fmt.Errorf("failed to deploy Niri config: %w", err) @@ -72,7 +81,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex } case deps.WindowManagerHyprland: if shouldReplaceConfig("Hyprland") { - result, err := cd.deployHyprlandConfig(terminal) + result, err := cd.deployHyprlandConfig(terminal, useSystemd) results = append(results, result) if err != nil { return results, fmt.Errorf("failed to deploy Hyprland config: %w", err) @@ -110,7 +119,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex return results, nil } -func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) { +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"), @@ -162,6 +171,10 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand) + if !useSystemd { + newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand) + } + if existingConfig != "" { mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) if err != nil { @@ -440,7 +453,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri } // deployHyprlandConfig handles Hyprland configuration deployment with backup and merging -func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) { +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"), @@ -472,7 +485,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) } - // Determine terminal command based on choice var terminalCommand string switch terminal { case deps.TerminalGhostty: @@ -482,12 +494,15 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme case deps.TerminalAlacritty: terminalCommand = "alacritty" default: - terminalCommand = "ghostty" // fallback to ghostty + terminalCommand = "ghostty" } newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand) - // If there was an existing config, merge the monitor sections + if !useSystemd { + newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand) + } + if existingConfig != "" { mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) if err != nil { @@ -510,24 +525,16 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme // 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) @@ -535,8 +542,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig return "", fmt.Errorf("could not find MONITOR CONFIG section") } - // Insert after the header - insertPos := headerMatch[1] + 1 // +1 for the newline + insertPos := headerMatch[1] + 1 var builder strings.Builder builder.WriteString(mergedConfig[:insertPos]) @@ -551,3 +557,69 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig 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") + 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", + "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" + 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) { + config = strings.Replace(config, + `spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`, + `spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms, + 1) + } + + return config +} diff --git a/core/internal/config/deployer_test.go b/core/internal/config/deployer_test.go index bc816cf7..995673bc 100644 --- a/core/internal/config/deployer_test.go +++ b/core/internal/config/deployer_test.go @@ -395,7 +395,7 @@ func TestHyprlandConfigDeployment(t *testing.T) { cd := NewConfigDeployer(logChan) t.Run("deploy hyprland config to empty directory", func(t *testing.T) { - result, err := cd.deployHyprlandConfig(deps.TerminalGhostty) + result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true) require.NoError(t, err) assert.Equal(t, "Hyprland", result.ConfigType) @@ -425,7 +425,7 @@ general { err = os.WriteFile(hyprPath, []byte(existingContent), 0644) require.NoError(t, err) - result, err := cd.deployHyprlandConfig(deps.TerminalKitty) + result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true) require.NoError(t, err) assert.Equal(t, "Hyprland", result.ConfigType) diff --git a/core/internal/distros/arch.go b/core/internal/distros/arch.go index cb77f219..6954978e 100644 --- a/core/internal/distros/arch.go +++ b/core/internal/distros/arch.go @@ -360,10 +360,8 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) } - if wm == deps.WindowManagerHyprland { - if err := a.WriteHyprlandSessionTarget(); err != nil { - a.log(fmt.Sprintf("Warning: failed to write hyprland session target: %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); err != nil { diff --git a/core/internal/distros/base.go b/core/internal/distros/base.go index 1c918dfc..5a092866 100644 --- a/core/internal/distros/base.go +++ b/core/internal/distros/base.go @@ -611,6 +611,15 @@ func (b *BaseDistribution) EnableDMSService(ctx context.Context) error { 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 { diff --git a/core/internal/distros/debian.go b/core/internal/distros/debian.go index 31f00802..e4e1c31b 100644 --- a/core/internal/distros/debian.go +++ b/core/internal/distros/debian.go @@ -338,6 +338,10 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [ 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); err != nil { d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) } diff --git a/core/internal/distros/fedora.go b/core/internal/distros/fedora.go index c5eeba6a..ab5988fd 100644 --- a/core/internal/distros/fedora.go +++ b/core/internal/distros/fedora.go @@ -359,6 +359,10 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [ 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); err != nil { f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) } diff --git a/core/internal/distros/gentoo.go b/core/internal/distros/gentoo.go index 60b58364..f3922f0a 100644 --- a/core/internal/distros/gentoo.go +++ b/core/internal/distros/gentoo.go @@ -436,6 +436,10 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [ 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); err != nil { g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) } diff --git a/core/internal/distros/opensuse.go b/core/internal/distros/opensuse.go index 4fc40c15..be00f9b9 100644 --- a/core/internal/distros/opensuse.go +++ b/core/internal/distros/opensuse.go @@ -377,6 +377,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies 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); err != nil { o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) } diff --git a/core/internal/distros/ubuntu.go b/core/internal/distros/ubuntu.go index f69e0552..c031f628 100644 --- a/core/internal/distros/ubuntu.go +++ b/core/internal/distros/ubuntu.go @@ -357,6 +357,10 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [ 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); err != nil { u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) }