refactor(shell): extract shell configuration into dedicated internal package
- commit
- f11c38c1707f612a2ea59285d02b383716b58b2e
- author
- bnema · · 2025-10-28 18:29 UTC
- committer
- bnema · · 2025-10-28 19:07 UTC
- parent
- bf2ccb9
- signature
- unsigned
2 files · +378 -0
A internal/shell/configurator.go +168 -0
| @@ -0,0 +1,168 @@ | |||
| 1 | + | package shell | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "path/filepath" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/archup/internal/config" | |
| 9 | + | "github.com/bnema/archup/internal/interfaces" | |
| 10 | + | "github.com/bnema/archup/internal/logger" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // ConfigResult holds the result of shell configuration | |
| 14 | + | type ConfigResult struct { | |
| 15 | + | ThemesApplied int | |
| 16 | + | ThemesFailed []string | |
| 17 | + | Warnings []string | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // Configurator handles shell and bash configuration setup | |
| 21 | + | type Configurator struct { | |
| 22 | + | fs interfaces.FileSystem | |
| 23 | + | http interfaces.HTTPClient | |
| 24 | + | chrExec interfaces.ChrootExecutor | |
| 25 | + | config *config.Config | |
| 26 | + | logger *logger.Logger | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // NewConfigurator creates a new shell configurator | |
| 30 | + | func NewConfigurator( | |
| 31 | + | fs interfaces.FileSystem, | |
| 32 | + | http interfaces.HTTPClient, | |
| 33 | + | chrExec interfaces.ChrootExecutor, | |
| 34 | + | cfg *config.Config, | |
| 35 | + | log *logger.Logger, | |
| 36 | + | ) *Configurator { | |
| 37 | + | return &Configurator{ | |
| 38 | + | fs: fs, | |
| 39 | + | http: http, | |
| 40 | + | chrExec: chrExec, | |
| 41 | + | config: cfg, | |
| 42 | + | logger: log, | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Configure sets up shell and CLI tool theming for a user | |
| 47 | + | func (c *Configurator) Configure(username, userHome string) (ConfigResult, error) { | |
| 48 | + | result := ConfigResult{ | |
| 49 | + | ThemesApplied: 0, | |
| 50 | + | ThemesFailed: []string{}, | |
| 51 | + | Warnings: []string{}, | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // Create archup shell directory structure | |
| 55 | + | archupDefault := filepath.Join(userHome, ".local", "share", "archup", "default") | |
| 56 | + | archupDefaultBash := filepath.Join(archupDefault, "bash") | |
| 57 | + | ||
| 58 | + | if err := c.fs.MkdirAll(archupDefaultBash, 0755); err != nil { | |
| 59 | + | return result, fmt.Errorf("failed to create shell config directory: %w", err) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Copy shell configuration files from templates | |
| 63 | + | if err := c.copyShellTemplates(archupDefault, archupDefaultBash); err != nil { | |
| 64 | + | return result, fmt.Errorf("failed to copy shell templates: %w", err) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Create .bashrc | |
| 68 | + | bashrcContent, err := c.readTemplate(config.BashrcTemplate) | |
| 69 | + | if err != nil { | |
| 70 | + | return result, fmt.Errorf("failed to read bashrc template: %w", err) | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | bashrcPath := filepath.Join(userHome, ".bashrc") | |
| 74 | + | if err := c.fs.WriteFile(bashrcPath, bashrcContent, 0644); err != nil { | |
| 75 | + | return result, fmt.Errorf("failed to write .bashrc: %w", err) | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // Set ownership to user | |
| 79 | + | if err := c.setShellOwnership(userHome, username); err != nil { | |
| 80 | + | return result, fmt.Errorf("failed to set ownership: %w", err) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Clone bleu-theme repository | |
| 84 | + | relativeHome := strings.TrimPrefix(userHome, config.PathMnt) | |
| 85 | + | themesDir := filepath.Join(relativeHome, ".local", "share", "archup", "themes") | |
| 86 | + | bleuThemeDir := filepath.Join(themesDir, "bleu") | |
| 87 | + | currentDir := filepath.Join(relativeHome, ".local", "share", "archup", "current") | |
| 88 | + | ||
| 89 | + | cloneCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && git clone https://github.com/bnema/bleu-theme.git %s'", | |
| 90 | + | username, themesDir, bleuThemeDir) | |
| 91 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cloneCmd); err != nil { | |
| 92 | + | result.Warnings = append(result.Warnings, "Failed to clone bleu-theme repository") | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // Create current theme symlink | |
| 96 | + | symlinkCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && ln -snf %s %s/theme'", | |
| 97 | + | username, currentDir, bleuThemeDir, currentDir) | |
| 98 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, symlinkCmd); err != nil { | |
| 99 | + | result.Warnings = append(result.Warnings, "Failed to create theme symlink") | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // Apply themes to CLI tools | |
| 103 | + | themeResult := c.applyThemes(username, userHome) | |
| 104 | + | result.ThemesApplied = themeResult.Applied | |
| 105 | + | result.ThemesFailed = themeResult.Failed | |
| 106 | + | result.Warnings = append(result.Warnings, themeResult.Warnings...) | |
| 107 | + | ||
| 108 | + | // Configure git delta | |
| 109 | + | gitCommands := []string{ | |
| 110 | + | fmt.Sprintf("su - %s -c 'git config --global core.pager delta'", username), | |
| 111 | + | fmt.Sprintf("su - %s -c 'git config --global interactive.diffFilter \"delta --color-only\"'", username), | |
| 112 | + | fmt.Sprintf("su - %s -c 'git config --global delta.navigate true'", username), | |
| 113 | + | fmt.Sprintf("su - %s -c 'git config --global delta.light false'", username), | |
| 114 | + | fmt.Sprintf("su - %s -c 'git config --global delta.side-by-side true'", username), | |
| 115 | + | fmt.Sprintf("su - %s -c 'git config --global merge.conflictstyle diff3'", username), | |
| 116 | + | fmt.Sprintf("su - %s -c 'git config --global diff.colorMoved default'", username), | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | for _, gitCmd := range gitCommands { | |
| 120 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, gitCmd); err != nil { | |
| 121 | + | result.Warnings = append(result.Warnings, "Failed to configure git delta") | |
| 122 | + | break | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | return result, nil | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // copyShellTemplates copies shell config templates to user directory | |
| 130 | + | func (c *Configurator) copyShellTemplates(archupDefault, archupDefaultBash string) error { | |
| 131 | + | templates := map[string]string{ | |
| 132 | + | config.ShellConfigTemplate: filepath.Join(archupDefaultBash, "shell"), | |
| 133 | + | config.ShellInitTemplate: filepath.Join(archupDefaultBash, "init"), | |
| 134 | + | config.ShellAliasesTemplate: filepath.Join(archupDefaultBash, "aliases"), | |
| 135 | + | config.ShellEnvsTemplate: filepath.Join(archupDefaultBash, "envs"), | |
| 136 | + | config.ShellRcTemplate: filepath.Join(archupDefaultBash, "rc"), | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | for template, dest := range templates { | |
| 140 | + | content, err := c.readTemplate(template) | |
| 141 | + | if err != nil { | |
| 142 | + | return fmt.Errorf("failed to read template %s: %w", template, err) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | if err := c.fs.WriteFile(dest, content, 0644); err != nil { | |
| 146 | + | return fmt.Errorf("failed to write %s: %w", dest, err) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | return nil | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // readTemplate reads a template file from install path | |
| 154 | + | func (c *Configurator) readTemplate(filename string) ([]byte, error) { | |
| 155 | + | // Use DefaultInstallDir directly to match where bootstrap downloads files | |
| 156 | + | templatePath := filepath.Join(config.DefaultInstallDir, filename) | |
| 157 | + | return c.fs.ReadFile(templatePath) | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | // setShellOwnership sets ownership of shell config files to user | |
| 161 | + | func (c *Configurator) setShellOwnership(userHome, username string) error { | |
| 162 | + | // Use chroot to run chown | |
| 163 | + | relativeHome := strings.TrimPrefix(userHome, config.PathMnt) | |
| 164 | + | chownCmd := fmt.Sprintf("chown -R %s:%s %s/.local %s/.bashrc", | |
| 165 | + | username, username, relativeHome, relativeHome) | |
| 166 | + | return c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, chownCmd) | |
| 167 | + | } | |
| 168 | + | ||
| @@ -0,0 +1,168 @@ | |||||
| 1 | + | package shell | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "strings" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/archup/internal/config" | |||
| 9 | + | "github.com/bnema/archup/internal/interfaces" | |||
| 10 | + | "github.com/bnema/archup/internal/logger" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // ConfigResult holds the result of shell configuration | |||
| 14 | + | type ConfigResult struct { | |||
| 15 | + | ThemesApplied int | |||
| 16 | + | ThemesFailed []string | |||
| 17 | + | Warnings []string | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // Configurator handles shell and bash configuration setup | |||
| 21 | + | type Configurator struct { | |||
| 22 | + | fs interfaces.FileSystem | |||
| 23 | + | http interfaces.HTTPClient | |||
| 24 | + | chrExec interfaces.ChrootExecutor | |||
| 25 | + | config *config.Config | |||
| 26 | + | logger *logger.Logger | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // NewConfigurator creates a new shell configurator | |||
| 30 | + | func NewConfigurator( | |||
| 31 | + | fs interfaces.FileSystem, | |||
| 32 | + | http interfaces.HTTPClient, | |||
| 33 | + | chrExec interfaces.ChrootExecutor, | |||
| 34 | + | cfg *config.Config, | |||
| 35 | + | log *logger.Logger, | |||
| 36 | + | ) *Configurator { | |||
| 37 | + | return &Configurator{ | |||
| 38 | + | fs: fs, | |||
| 39 | + | http: http, | |||
| 40 | + | chrExec: chrExec, | |||
| 41 | + | config: cfg, | |||
| 42 | + | logger: log, | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Configure sets up shell and CLI tool theming for a user | |||
| 47 | + | func (c *Configurator) Configure(username, userHome string) (ConfigResult, error) { | |||
| 48 | + | result := ConfigResult{ | |||
| 49 | + | ThemesApplied: 0, | |||
| 50 | + | ThemesFailed: []string{}, | |||
| 51 | + | Warnings: []string{}, | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // Create archup shell directory structure | |||
| 55 | + | archupDefault := filepath.Join(userHome, ".local", "share", "archup", "default") | |||
| 56 | + | archupDefaultBash := filepath.Join(archupDefault, "bash") | |||
| 57 | + | ||||
| 58 | + | if err := c.fs.MkdirAll(archupDefaultBash, 0755); err != nil { | |||
| 59 | + | return result, fmt.Errorf("failed to create shell config directory: %w", err) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Copy shell configuration files from templates | |||
| 63 | + | if err := c.copyShellTemplates(archupDefault, archupDefaultBash); err != nil { | |||
| 64 | + | return result, fmt.Errorf("failed to copy shell templates: %w", err) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Create .bashrc | |||
| 68 | + | bashrcContent, err := c.readTemplate(config.BashrcTemplate) | |||
| 69 | + | if err != nil { | |||
| 70 | + | return result, fmt.Errorf("failed to read bashrc template: %w", err) | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | bashrcPath := filepath.Join(userHome, ".bashrc") | |||
| 74 | + | if err := c.fs.WriteFile(bashrcPath, bashrcContent, 0644); err != nil { | |||
| 75 | + | return result, fmt.Errorf("failed to write .bashrc: %w", err) | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // Set ownership to user | |||
| 79 | + | if err := c.setShellOwnership(userHome, username); err != nil { | |||
| 80 | + | return result, fmt.Errorf("failed to set ownership: %w", err) | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // Clone bleu-theme repository | |||
| 84 | + | relativeHome := strings.TrimPrefix(userHome, config.PathMnt) | |||
| 85 | + | themesDir := filepath.Join(relativeHome, ".local", "share", "archup", "themes") | |||
| 86 | + | bleuThemeDir := filepath.Join(themesDir, "bleu") | |||
| 87 | + | currentDir := filepath.Join(relativeHome, ".local", "share", "archup", "current") | |||
| 88 | + | ||||
| 89 | + | cloneCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && git clone https://github.com/bnema/bleu-theme.git %s'", | |||
| 90 | + | username, themesDir, bleuThemeDir) | |||
| 91 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cloneCmd); err != nil { | |||
| 92 | + | result.Warnings = append(result.Warnings, "Failed to clone bleu-theme repository") | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // Create current theme symlink | |||
| 96 | + | symlinkCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && ln -snf %s %s/theme'", | |||
| 97 | + | username, currentDir, bleuThemeDir, currentDir) | |||
| 98 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, symlinkCmd); err != nil { | |||
| 99 | + | result.Warnings = append(result.Warnings, "Failed to create theme symlink") | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // Apply themes to CLI tools | |||
| 103 | + | themeResult := c.applyThemes(username, userHome) | |||
| 104 | + | result.ThemesApplied = themeResult.Applied | |||
| 105 | + | result.ThemesFailed = themeResult.Failed | |||
| 106 | + | result.Warnings = append(result.Warnings, themeResult.Warnings...) | |||
| 107 | + | ||||
| 108 | + | // Configure git delta | |||
| 109 | + | gitCommands := []string{ | |||
| 110 | + | fmt.Sprintf("su - %s -c 'git config --global core.pager delta'", username), | |||
| 111 | + | fmt.Sprintf("su - %s -c 'git config --global interactive.diffFilter \"delta --color-only\"'", username), | |||
| 112 | + | fmt.Sprintf("su - %s -c 'git config --global delta.navigate true'", username), | |||
| 113 | + | fmt.Sprintf("su - %s -c 'git config --global delta.light false'", username), | |||
| 114 | + | fmt.Sprintf("su - %s -c 'git config --global delta.side-by-side true'", username), | |||
| 115 | + | fmt.Sprintf("su - %s -c 'git config --global merge.conflictstyle diff3'", username), | |||
| 116 | + | fmt.Sprintf("su - %s -c 'git config --global diff.colorMoved default'", username), | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | for _, gitCmd := range gitCommands { | |||
| 120 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, gitCmd); err != nil { | |||
| 121 | + | result.Warnings = append(result.Warnings, "Failed to configure git delta") | |||
| 122 | + | break | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | return result, nil | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // copyShellTemplates copies shell config templates to user directory | |||
| 130 | + | func (c *Configurator) copyShellTemplates(archupDefault, archupDefaultBash string) error { | |||
| 131 | + | templates := map[string]string{ | |||
| 132 | + | config.ShellConfigTemplate: filepath.Join(archupDefaultBash, "shell"), | |||
| 133 | + | config.ShellInitTemplate: filepath.Join(archupDefaultBash, "init"), | |||
| 134 | + | config.ShellAliasesTemplate: filepath.Join(archupDefaultBash, "aliases"), | |||
| 135 | + | config.ShellEnvsTemplate: filepath.Join(archupDefaultBash, "envs"), | |||
| 136 | + | config.ShellRcTemplate: filepath.Join(archupDefaultBash, "rc"), | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | for template, dest := range templates { | |||
| 140 | + | content, err := c.readTemplate(template) | |||
| 141 | + | if err != nil { | |||
| 142 | + | return fmt.Errorf("failed to read template %s: %w", template, err) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | if err := c.fs.WriteFile(dest, content, 0644); err != nil { | |||
| 146 | + | return fmt.Errorf("failed to write %s: %w", dest, err) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | return nil | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // readTemplate reads a template file from install path | |||
| 154 | + | func (c *Configurator) readTemplate(filename string) ([]byte, error) { | |||
| 155 | + | // Use DefaultInstallDir directly to match where bootstrap downloads files | |||
| 156 | + | templatePath := filepath.Join(config.DefaultInstallDir, filename) | |||
| 157 | + | return c.fs.ReadFile(templatePath) | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | // setShellOwnership sets ownership of shell config files to user | |||
| 161 | + | func (c *Configurator) setShellOwnership(userHome, username string) error { | |||
| 162 | + | // Use chroot to run chown | |||
| 163 | + | relativeHome := strings.TrimPrefix(userHome, config.PathMnt) | |||
| 164 | + | chownCmd := fmt.Sprintf("chown -R %s:%s %s/.local %s/.bashrc", | |||
| 165 | + | username, username, relativeHome, relativeHome) | |||
| 166 | + | return c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, chownCmd) | |||
| 167 | + | } | |||
| 168 | + | ||||
A internal/shell/themes.go +210 -0
| @@ -0,0 +1,210 @@ | |||
| 1 | + | package shell | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "path/filepath" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/archup/internal/config" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // ThemeResult holds the result of theme application | |
| 12 | + | type ThemeResult struct { | |
| 13 | + | Applied int | |
| 14 | + | Failed []string | |
| 15 | + | Warnings []string | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // applyThemes orchestrates centralized theme symlink setup for all CLI tools | |
| 19 | + | func (c *Configurator) applyThemes(username, userHome string) ThemeResult { | |
| 20 | + | result := ThemeResult{ | |
| 21 | + | Applied: 0, | |
| 22 | + | Failed: []string{}, | |
| 23 | + | Warnings: []string{}, | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // Strip /mnt prefix for use in chroot commands | |
| 27 | + | relativeHome := strings.TrimPrefix(userHome, config.PathMnt) | |
| 28 | + | ||
| 29 | + | // Step 1: Setup central theme directory structure | |
| 30 | + | if err := c.setupCentralThemeDirectory(username, relativeHome); err != nil { | |
| 31 | + | result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to setup central theme directory: %v", err)) | |
| 32 | + | return result | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Step 2: Create central symlinks pointing to active theme (bleu) | |
| 36 | + | if err := c.createCentralSymlinks(username, relativeHome, "bleu"); err != nil { | |
| 37 | + | result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to create central symlinks: %v", err)) | |
| 38 | + | return result | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Step 3: Apply each tool's theme configuration | |
| 42 | + | tools := []struct { | |
| 43 | + | name string | |
| 44 | + | apply func(string, string) error | |
| 45 | + | }{ | |
| 46 | + | {"starship", c.applyStarshipTheme}, | |
| 47 | + | {"eza", c.applyEzaTheme}, | |
| 48 | + | {"bat", c.applyBatTheme}, | |
| 49 | + | {"btop", c.applyBtopTheme}, | |
| 50 | + | {"yazi", c.applyYaziTheme}, | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | for _, tool := range tools { | |
| 54 | + | if err := tool.apply(username, relativeHome); err != nil { | |
| 55 | + | result.Failed = append(result.Failed, tool.name) | |
| 56 | + | result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to configure %s: %v", tool.name, err)) | |
| 57 | + | } else { | |
| 58 | + | result.Applied++ | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | return result | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // setupCentralThemeDirectory creates ~/.local/share/archup/themes/current/ | |
| 66 | + | func (c *Configurator) setupCentralThemeDirectory(username, relativeHome string) error { | |
| 67 | + | centralThemeDir := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current") | |
| 68 | + | ||
| 69 | + | mkdirCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, centralThemeDir) | |
| 70 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, mkdirCmd); err != nil { | |
| 71 | + | return fmt.Errorf("failed to create central theme directory: %w", err) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | return nil | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // createCentralSymlinks creates symlinks in themes/current/ pointing to active theme files | |
| 78 | + | func (c *Configurator) createCentralSymlinks(username, relativeHome, activeTheme string) error { | |
| 79 | + | themesDir := filepath.Join(relativeHome, ".local", "share", "archup", "themes") | |
| 80 | + | centralDir := filepath.Join(themesDir, "current") | |
| 81 | + | themeSource := filepath.Join(themesDir, activeTheme) | |
| 82 | + | ||
| 83 | + | // Map of symlink name -> target path | |
| 84 | + | symlinks := map[string]string{ | |
| 85 | + | "starship.toml": filepath.Join(themeSource, "starship", "bleu.toml"), | |
| 86 | + | "eza.yml": filepath.Join(themeSource, "eza", "theme.yml"), | |
| 87 | + | "bat.tmTheme": filepath.Join(themeSource, "bat", "bleu.tmTheme"), | |
| 88 | + | "btop.theme": filepath.Join(themeSource, "btop", "bleu.theme"), | |
| 89 | + | "yazi.toml": filepath.Join(themeSource, "yazi", "bleu.toml"), | |
| 90 | + | "fzf.sh": filepath.Join(themeSource, "fzf", "bleu.sh"), | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | for linkName, target := range symlinks { | |
| 94 | + | linkPath := filepath.Join(centralDir, linkName) | |
| 95 | + | ||
| 96 | + | // Use ln -snf: -s for symbolic, -n to handle existing symlink as file, -f to force overwrite | |
| 97 | + | symlinkCmd := fmt.Sprintf("su - %s -c 'ln -snf %s %s'", | |
| 98 | + | username, target, linkPath) | |
| 99 | + | ||
| 100 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, symlinkCmd); err != nil { | |
| 101 | + | return fmt.Errorf("failed to create symlink %s -> %s: %w", linkName, target, err) | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | return nil | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // applyStarshipTheme configures starship to use central symlink | |
| 109 | + | // Note: Actual configuration happens via STARSHIP_CONFIG env var in configs/shell/init | |
| 110 | + | func (c *Configurator) applyStarshipTheme(username, relativeHome string) error { | |
| 111 | + | // Starship uses STARSHIP_CONFIG environment variable pointing to themes/current/starship.toml | |
| 112 | + | // No additional setup needed - the symlink is already created by createCentralSymlinks() | |
| 113 | + | // Shell config will be updated separately to export STARSHIP_CONFIG | |
| 114 | + | return nil | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // applyEzaTheme creates symlink in eza config directory pointing to central theme | |
| 118 | + | func (c *Configurator) applyEzaTheme(username, relativeHome string) error { | |
| 119 | + | ezaConfigDir := filepath.Join(relativeHome, ".config", "eza") | |
| 120 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "eza.yml") | |
| 121 | + | ezaThemeLink := filepath.Join(ezaConfigDir, "theme.yml") | |
| 122 | + | ||
| 123 | + | commands := []string{ | |
| 124 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, ezaConfigDir), | |
| 125 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, ezaThemeLink), | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | for _, cmd := range commands { | |
| 129 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |
| 130 | + | return fmt.Errorf("eza theme setup failed: %w", err) | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | return nil | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // applyBatTheme creates symlink in bat themes directory and configures bat.conf | |
| 138 | + | func (c *Configurator) applyBatTheme(username, relativeHome string) error { | |
| 139 | + | batThemesDir := filepath.Join(relativeHome, ".config", "bat", "themes") | |
| 140 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "bat.tmTheme") | |
| 141 | + | batThemeLink := filepath.Join(batThemesDir, "current.tmTheme") | |
| 142 | + | ||
| 143 | + | commands := []string{ | |
| 144 | + | // Create bat themes directory | |
| 145 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, batThemesDir), | |
| 146 | + | ||
| 147 | + | // Create symlink: ~/.config/bat/themes/current.tmTheme -> themes/current/bat.tmTheme | |
| 148 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, batThemeLink), | |
| 149 | + | ||
| 150 | + | // Create bat config referencing "current" theme | |
| 151 | + | fmt.Sprintf("su - %s -c 'echo \"--theme=\\\"current\\\"\" > ~/.config/bat/bat.conf'", username), | |
| 152 | + | ||
| 153 | + | // Build bat cache to register the theme | |
| 154 | + | fmt.Sprintf("su - %s -c 'bat cache --build'", username), | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | for _, cmd := range commands { | |
| 158 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |
| 159 | + | return fmt.Errorf("bat theme setup failed: %w", err) | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | return nil | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // applyBtopTheme creates symlink in btop themes directory | |
| 167 | + | func (c *Configurator) applyBtopTheme(username, relativeHome string) error { | |
| 168 | + | btopThemesDir := filepath.Join(relativeHome, ".config", "btop", "themes") | |
| 169 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "btop.theme") | |
| 170 | + | btopThemeLink := filepath.Join(btopThemesDir, "current.theme") | |
| 171 | + | ||
| 172 | + | commands := []string{ | |
| 173 | + | // Create btop themes directory | |
| 174 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, btopThemesDir), | |
| 175 | + | ||
| 176 | + | // Create symlink: ~/.config/btop/themes/current.theme -> themes/current/btop.theme | |
| 177 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, btopThemeLink), | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | for _, cmd := range commands { | |
| 181 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |
| 182 | + | return fmt.Errorf("btop theme setup failed: %w", err) | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | return nil | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | // applyYaziTheme creates yazi flavor directory with symlink to central theme | |
| 190 | + | func (c *Configurator) applyYaziTheme(username, relativeHome string) error { | |
| 191 | + | yaziFlavorDir := filepath.Join(relativeHome, ".config", "yazi", "flavors", "current") | |
| 192 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "yazi.toml") | |
| 193 | + | yaziFlavorLink := filepath.Join(yaziFlavorDir, "flavor.toml") | |
| 194 | + | ||
| 195 | + | commands := []string{ | |
| 196 | + | // Create yazi flavor directory for "current" theme | |
| 197 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, yaziFlavorDir), | |
| 198 | + | ||
| 199 | + | // Create symlink: ~/.config/yazi/flavors/current/flavor.toml -> themes/current/yazi.toml | |
| 200 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, yaziFlavorLink), | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | for _, cmd := range commands { | |
| 204 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |
| 205 | + | return fmt.Errorf("yazi theme setup failed: %w", err) | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | return nil | |
| 210 | + | } | |
| @@ -0,0 +1,210 @@ | |||||
| 1 | + | package shell | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "strings" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/archup/internal/config" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // ThemeResult holds the result of theme application | |||
| 12 | + | type ThemeResult struct { | |||
| 13 | + | Applied int | |||
| 14 | + | Failed []string | |||
| 15 | + | Warnings []string | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // applyThemes orchestrates centralized theme symlink setup for all CLI tools | |||
| 19 | + | func (c *Configurator) applyThemes(username, userHome string) ThemeResult { | |||
| 20 | + | result := ThemeResult{ | |||
| 21 | + | Applied: 0, | |||
| 22 | + | Failed: []string{}, | |||
| 23 | + | Warnings: []string{}, | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // Strip /mnt prefix for use in chroot commands | |||
| 27 | + | relativeHome := strings.TrimPrefix(userHome, config.PathMnt) | |||
| 28 | + | ||||
| 29 | + | // Step 1: Setup central theme directory structure | |||
| 30 | + | if err := c.setupCentralThemeDirectory(username, relativeHome); err != nil { | |||
| 31 | + | result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to setup central theme directory: %v", err)) | |||
| 32 | + | return result | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Step 2: Create central symlinks pointing to active theme (bleu) | |||
| 36 | + | if err := c.createCentralSymlinks(username, relativeHome, "bleu"); err != nil { | |||
| 37 | + | result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to create central symlinks: %v", err)) | |||
| 38 | + | return result | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Step 3: Apply each tool's theme configuration | |||
| 42 | + | tools := []struct { | |||
| 43 | + | name string | |||
| 44 | + | apply func(string, string) error | |||
| 45 | + | }{ | |||
| 46 | + | {"starship", c.applyStarshipTheme}, | |||
| 47 | + | {"eza", c.applyEzaTheme}, | |||
| 48 | + | {"bat", c.applyBatTheme}, | |||
| 49 | + | {"btop", c.applyBtopTheme}, | |||
| 50 | + | {"yazi", c.applyYaziTheme}, | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | for _, tool := range tools { | |||
| 54 | + | if err := tool.apply(username, relativeHome); err != nil { | |||
| 55 | + | result.Failed = append(result.Failed, tool.name) | |||
| 56 | + | result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to configure %s: %v", tool.name, err)) | |||
| 57 | + | } else { | |||
| 58 | + | result.Applied++ | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | return result | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // setupCentralThemeDirectory creates ~/.local/share/archup/themes/current/ | |||
| 66 | + | func (c *Configurator) setupCentralThemeDirectory(username, relativeHome string) error { | |||
| 67 | + | centralThemeDir := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current") | |||
| 68 | + | ||||
| 69 | + | mkdirCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, centralThemeDir) | |||
| 70 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, mkdirCmd); err != nil { | |||
| 71 | + | return fmt.Errorf("failed to create central theme directory: %w", err) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | return nil | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // createCentralSymlinks creates symlinks in themes/current/ pointing to active theme files | |||
| 78 | + | func (c *Configurator) createCentralSymlinks(username, relativeHome, activeTheme string) error { | |||
| 79 | + | themesDir := filepath.Join(relativeHome, ".local", "share", "archup", "themes") | |||
| 80 | + | centralDir := filepath.Join(themesDir, "current") | |||
| 81 | + | themeSource := filepath.Join(themesDir, activeTheme) | |||
| 82 | + | ||||
| 83 | + | // Map of symlink name -> target path | |||
| 84 | + | symlinks := map[string]string{ | |||
| 85 | + | "starship.toml": filepath.Join(themeSource, "starship", "bleu.toml"), | |||
| 86 | + | "eza.yml": filepath.Join(themeSource, "eza", "theme.yml"), | |||
| 87 | + | "bat.tmTheme": filepath.Join(themeSource, "bat", "bleu.tmTheme"), | |||
| 88 | + | "btop.theme": filepath.Join(themeSource, "btop", "bleu.theme"), | |||
| 89 | + | "yazi.toml": filepath.Join(themeSource, "yazi", "bleu.toml"), | |||
| 90 | + | "fzf.sh": filepath.Join(themeSource, "fzf", "bleu.sh"), | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | for linkName, target := range symlinks { | |||
| 94 | + | linkPath := filepath.Join(centralDir, linkName) | |||
| 95 | + | ||||
| 96 | + | // Use ln -snf: -s for symbolic, -n to handle existing symlink as file, -f to force overwrite | |||
| 97 | + | symlinkCmd := fmt.Sprintf("su - %s -c 'ln -snf %s %s'", | |||
| 98 | + | username, target, linkPath) | |||
| 99 | + | ||||
| 100 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, symlinkCmd); err != nil { | |||
| 101 | + | return fmt.Errorf("failed to create symlink %s -> %s: %w", linkName, target, err) | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | return nil | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // applyStarshipTheme configures starship to use central symlink | |||
| 109 | + | // Note: Actual configuration happens via STARSHIP_CONFIG env var in configs/shell/init | |||
| 110 | + | func (c *Configurator) applyStarshipTheme(username, relativeHome string) error { | |||
| 111 | + | // Starship uses STARSHIP_CONFIG environment variable pointing to themes/current/starship.toml | |||
| 112 | + | // No additional setup needed - the symlink is already created by createCentralSymlinks() | |||
| 113 | + | // Shell config will be updated separately to export STARSHIP_CONFIG | |||
| 114 | + | return nil | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // applyEzaTheme creates symlink in eza config directory pointing to central theme | |||
| 118 | + | func (c *Configurator) applyEzaTheme(username, relativeHome string) error { | |||
| 119 | + | ezaConfigDir := filepath.Join(relativeHome, ".config", "eza") | |||
| 120 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "eza.yml") | |||
| 121 | + | ezaThemeLink := filepath.Join(ezaConfigDir, "theme.yml") | |||
| 122 | + | ||||
| 123 | + | commands := []string{ | |||
| 124 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, ezaConfigDir), | |||
| 125 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, ezaThemeLink), | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | for _, cmd := range commands { | |||
| 129 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |||
| 130 | + | return fmt.Errorf("eza theme setup failed: %w", err) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | return nil | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // applyBatTheme creates symlink in bat themes directory and configures bat.conf | |||
| 138 | + | func (c *Configurator) applyBatTheme(username, relativeHome string) error { | |||
| 139 | + | batThemesDir := filepath.Join(relativeHome, ".config", "bat", "themes") | |||
| 140 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "bat.tmTheme") | |||
| 141 | + | batThemeLink := filepath.Join(batThemesDir, "current.tmTheme") | |||
| 142 | + | ||||
| 143 | + | commands := []string{ | |||
| 144 | + | // Create bat themes directory | |||
| 145 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, batThemesDir), | |||
| 146 | + | ||||
| 147 | + | // Create symlink: ~/.config/bat/themes/current.tmTheme -> themes/current/bat.tmTheme | |||
| 148 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, batThemeLink), | |||
| 149 | + | ||||
| 150 | + | // Create bat config referencing "current" theme | |||
| 151 | + | fmt.Sprintf("su - %s -c 'echo \"--theme=\\\"current\\\"\" > ~/.config/bat/bat.conf'", username), | |||
| 152 | + | ||||
| 153 | + | // Build bat cache to register the theme | |||
| 154 | + | fmt.Sprintf("su - %s -c 'bat cache --build'", username), | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | for _, cmd := range commands { | |||
| 158 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |||
| 159 | + | return fmt.Errorf("bat theme setup failed: %w", err) | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | return nil | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // applyBtopTheme creates symlink in btop themes directory | |||
| 167 | + | func (c *Configurator) applyBtopTheme(username, relativeHome string) error { | |||
| 168 | + | btopThemesDir := filepath.Join(relativeHome, ".config", "btop", "themes") | |||
| 169 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "btop.theme") | |||
| 170 | + | btopThemeLink := filepath.Join(btopThemesDir, "current.theme") | |||
| 171 | + | ||||
| 172 | + | commands := []string{ | |||
| 173 | + | // Create btop themes directory | |||
| 174 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, btopThemesDir), | |||
| 175 | + | ||||
| 176 | + | // Create symlink: ~/.config/btop/themes/current.theme -> themes/current/btop.theme | |||
| 177 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, btopThemeLink), | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | for _, cmd := range commands { | |||
| 181 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |||
| 182 | + | return fmt.Errorf("btop theme setup failed: %w", err) | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | return nil | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | // applyYaziTheme creates yazi flavor directory with symlink to central theme | |||
| 190 | + | func (c *Configurator) applyYaziTheme(username, relativeHome string) error { | |||
| 191 | + | yaziFlavorDir := filepath.Join(relativeHome, ".config", "yazi", "flavors", "current") | |||
| 192 | + | centralTheme := filepath.Join(relativeHome, ".local", "share", "archup", "themes", "current", "yazi.toml") | |||
| 193 | + | yaziFlavorLink := filepath.Join(yaziFlavorDir, "flavor.toml") | |||
| 194 | + | ||||
| 195 | + | commands := []string{ | |||
| 196 | + | // Create yazi flavor directory for "current" theme | |||
| 197 | + | fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, yaziFlavorDir), | |||
| 198 | + | ||||
| 199 | + | // Create symlink: ~/.config/yazi/flavors/current/flavor.toml -> themes/current/yazi.toml | |||
| 200 | + | fmt.Sprintf("su - %s -c 'ln -snf %s %s'", username, centralTheme, yaziFlavorLink), | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | for _, cmd := range commands { | |||
| 204 | + | if err := c.chrExec.ChrootExec(c.logger.LogPath(), config.PathMnt, cmd); err != nil { | |||
| 205 | + | return fmt.Errorf("yazi theme setup failed: %w", err) | |||
| 206 | + | } | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | return nil | |||
| 210 | + | } | |||