detached f11c38c

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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close