detached 935f0cf

feat(verify): add shell configuration and theme verification utilities

commit
935f0cf0e769b082b288f1773c01cbd24edde0a2
author
bnema · · 2025-10-28 19:45 UTC
committer
bnema · · 2025-10-28 20:39 UTC
parent
f11c38c
signature
unsigned
2 files · +260 -0
A internal/verify/shell.go +87 -0
@@ -0,0 +1,87 @@
1+package verify
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+)
11+
12+// ShellConfig holds verification results for shell configuration
13+type ShellConfig struct {
14+ BashrcExists bool
15+ ShellFilesOK bool
16+ BashrcSyntaxOK bool
17+ Warnings []string
18+ Errors []string
19+}
20+
21+// ValidateShellConfigs performs comprehensive shell configuration verification
22+func ValidateShellConfigs(
23+ fs interfaces.FileSystem,
24+ chrExec interfaces.ChrootExecutor,
25+ logPath string,
26+ userHome string,
27+) ShellConfig {
28+ result := ShellConfig{
29+ Warnings: []string{},
30+ Errors: []string{},
31+ }
32+
33+ // Check .bashrc exists
34+ bashrcPath := filepath.Join(userHome, ".bashrc")
35+ _, err := fs.Stat(bashrcPath)
36+ if fs.IsNotExist(err) {
37+ result.BashrcExists = false
38+ result.Warnings = append(result.Warnings, ".bashrc not found")
39+ } else if err != nil {
40+ result.Warnings = append(result.Warnings, fmt.Sprintf(".bashrc stat error: %v", err))
41+ } else {
42+ result.BashrcExists = true
43+ }
44+
45+ // Check shell config directory exists
46+ archupDefaultBash := filepath.Join(userHome, ".local", "share", "archup", "default", "bash")
47+ _, err = fs.Stat(archupDefaultBash)
48+ if fs.IsNotExist(err) {
49+ result.Warnings = append(result.Warnings, "shell config directory missing")
50+ } else if err == nil {
51+ result.ShellFilesOK = true
52+ } else {
53+ result.Warnings = append(result.Warnings, fmt.Sprintf("shell config directory stat error: %v", err))
54+ }
55+
56+ return result
57+}
58+
59+// ValidateBashSyntax tests bash configuration syntax by loading bash in chroot
60+func ValidateBashSyntax(
61+ chrExec interfaces.ChrootExecutor,
62+ logPath string,
63+) error {
64+ // Test bash syntax: bash -n ~/.bashrc in chroot
65+ cmd := "bash -n /root/.bashrc"
66+ if err := chrExec.ChrootExec(logPath, config.PathMnt, cmd); err != nil {
67+ return fmt.Errorf("bash syntax check failed: %w", err)
68+ }
69+ return nil
70+}
71+
72+// GetShellConfigWarnings returns formatted warning messages
73+func GetShellConfigWarnings(result ShellConfig) string {
74+ if len(result.Warnings) == 0 && len(result.Errors) == 0 {
75+ return ""
76+ }
77+
78+ var msgs []string
79+ for _, w := range result.Warnings {
80+ msgs = append(msgs, fmt.Sprintf("[WARN] %s", w))
81+ }
82+ for _, e := range result.Errors {
83+ msgs = append(msgs, fmt.Sprintf("[ERROR] %s", e))
84+ }
85+
86+ return strings.Join(msgs, "\n")
87+}
@@ -0,0 +1,87 @@
1+package verify
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+)
11+
12+// ShellConfig holds verification results for shell configuration
13+type ShellConfig struct {
14+ BashrcExists bool
15+ ShellFilesOK bool
16+ BashrcSyntaxOK bool
17+ Warnings []string
18+ Errors []string
19+}
20+
21+// ValidateShellConfigs performs comprehensive shell configuration verification
22+func ValidateShellConfigs(
23+ fs interfaces.FileSystem,
24+ chrExec interfaces.ChrootExecutor,
25+ logPath string,
26+ userHome string,
27+) ShellConfig {
28+ result := ShellConfig{
29+ Warnings: []string{},
30+ Errors: []string{},
31+ }
32+
33+ // Check .bashrc exists
34+ bashrcPath := filepath.Join(userHome, ".bashrc")
35+ _, err := fs.Stat(bashrcPath)
36+ if fs.IsNotExist(err) {
37+ result.BashrcExists = false
38+ result.Warnings = append(result.Warnings, ".bashrc not found")
39+ } else if err != nil {
40+ result.Warnings = append(result.Warnings, fmt.Sprintf(".bashrc stat error: %v", err))
41+ } else {
42+ result.BashrcExists = true
43+ }
44+
45+ // Check shell config directory exists
46+ archupDefaultBash := filepath.Join(userHome, ".local", "share", "archup", "default", "bash")
47+ _, err = fs.Stat(archupDefaultBash)
48+ if fs.IsNotExist(err) {
49+ result.Warnings = append(result.Warnings, "shell config directory missing")
50+ } else if err == nil {
51+ result.ShellFilesOK = true
52+ } else {
53+ result.Warnings = append(result.Warnings, fmt.Sprintf("shell config directory stat error: %v", err))
54+ }
55+
56+ return result
57+}
58+
59+// ValidateBashSyntax tests bash configuration syntax by loading bash in chroot
60+func ValidateBashSyntax(
61+ chrExec interfaces.ChrootExecutor,
62+ logPath string,
63+) error {
64+ // Test bash syntax: bash -n ~/.bashrc in chroot
65+ cmd := "bash -n /root/.bashrc"
66+ if err := chrExec.ChrootExec(logPath, config.PathMnt, cmd); err != nil {
67+ return fmt.Errorf("bash syntax check failed: %w", err)
68+ }
69+ return nil
70+}
71+
72+// GetShellConfigWarnings returns formatted warning messages
73+func GetShellConfigWarnings(result ShellConfig) string {
74+ if len(result.Warnings) == 0 && len(result.Errors) == 0 {
75+ return ""
76+ }
77+
78+ var msgs []string
79+ for _, w := range result.Warnings {
80+ msgs = append(msgs, fmt.Sprintf("[WARN] %s", w))
81+ }
82+ for _, e := range result.Errors {
83+ msgs = append(msgs, fmt.Sprintf("[ERROR] %s", e))
84+ }
85+
86+ return strings.Join(msgs, "\n")
87+}
A internal/verify/themes.go +173 -0
@@ -0,0 +1,173 @@
1+package verify
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+)
11+
12+// ThemeVerification holds results of theme verification
13+type ThemeVerification struct {
14+ BleuThemeCloned bool
15+ ThemeFilesOK map[string]bool // Tool name -> exists
16+ ConfigFilesOK map[string]bool // Tool name -> config file exists
17+ BatCacheBuilt bool
18+ StarshipValid bool
19+ Warnings []string
20+ Errors []string
21+}
22+
23+// NewThemeVerification creates a new theme verification result
24+func NewThemeVerification() *ThemeVerification {
25+ return &ThemeVerification{
26+ ThemeFilesOK: make(map[string]bool),
27+ ConfigFilesOK: make(map[string]bool),
28+ Warnings: []string{},
29+ Errors: []string{},
30+ }
31+}
32+
33+// ValidateBleuThemeClone verifies the bleu-theme repository was successfully cloned
34+func ValidateBleuThemeClone(fs interfaces.FileSystem, userHome string) bool {
35+ bleuThemeDir := filepath.Join(userHome, ".local", "share", "archup", "themes", "bleu")
36+ gitDir := filepath.Join(bleuThemeDir, ".git")
37+
38+ // Check if .git directory exists (indicates successful clone)
39+ _, err := fs.Stat(gitDir)
40+ return err == nil
41+}
42+
43+// ValidateThemeFiles checks if expected theme files exist after cloning
44+func ValidateThemeFiles(fs interfaces.FileSystem, userHome string) *ThemeVerification {
45+ result := NewThemeVerification()
46+ themeDir := filepath.Join(userHome, ".local", "share", "archup", "themes", "bleu")
47+
48+ // Check clone first
49+ result.BleuThemeCloned = ValidateBleuThemeClone(fs, userHome)
50+ if !result.BleuThemeCloned {
51+ result.Warnings = append(result.Warnings, "bleu-theme not cloned successfully")
52+ return result
53+ }
54+
55+ // Theme file paths to check
56+ themeFiles := map[string]string{
57+ "starship": filepath.Join(themeDir, "starship", "bleu.toml"),
58+ "eza": filepath.Join(themeDir, "eza", "theme.yml"),
59+ "bat": filepath.Join(themeDir, "bat", "bleu.tmTheme"),
60+ "btop": filepath.Join(themeDir, "btop", "bleu.theme"),
61+ "yazi": filepath.Join(themeDir, "yazi", "bleu.toml"),
62+ "fzf": filepath.Join(themeDir, "fzf", "bleu.sh"),
63+ }
64+
65+ for tool, path := range themeFiles {
66+ _, err := fs.Stat(path)
67+ exists := err == nil
68+ result.ThemeFilesOK[tool] = exists
69+ if !exists {
70+ result.Warnings = append(result.Warnings, fmt.Sprintf("%s theme file missing: %s", tool, path))
71+ }
72+ }
73+
74+ return result
75+}
76+
77+// ValidateConfigFiles checks if configuration files were successfully copied
78+func ValidateConfigFiles(fs interfaces.FileSystem, userHome string) *ThemeVerification {
79+ result := NewThemeVerification()
80+
81+ configFiles := map[string]string{
82+ "starship": filepath.Join(userHome, ".local", "share", "archup", "default", "starship.toml"),
83+ "eza": filepath.Join(userHome, ".config", "eza", "theme.yml"),
84+ "bat": filepath.Join(userHome, ".config", "bat", "themes", "bleu.tmTheme"),
85+ "btop": filepath.Join(userHome, ".config", "btop", "themes", "bleu.theme"),
86+ "yazi": filepath.Join(userHome, ".config", "yazi", "flavors", "bleu", "flavor.toml"),
87+ }
88+
89+ for tool, path := range configFiles {
90+ _, err := fs.Stat(path)
91+ exists := err == nil
92+ result.ConfigFilesOK[tool] = exists
93+ if !exists {
94+ result.Warnings = append(result.Warnings, fmt.Sprintf("%s config not found: %s", tool, path))
95+ }
96+ }
97+
98+ return result
99+}
100+
101+// ValidateBatCache checks if bat cache was built successfully
102+// This would require running `bat --list-themes` in chroot
103+func ValidateBatCache(chrExec interfaces.ChrootExecutor, logPath string) bool {
104+ cmd := "bat --list-themes | grep -i bleu"
105+ err := chrExec.ChrootExec(logPath, config.PathMnt, cmd)
106+ return err == nil
107+}
108+
109+// ValidateStarshipConfig tests if starship can load its configuration
110+func ValidateStarshipConfig(chrExec interfaces.ChrootExecutor, logPath string) bool {
111+ cmd := "starship config"
112+ err := chrExec.ChrootExec(logPath, config.PathMnt, cmd)
113+ return err == nil
114+}
115+
116+// GetThemeVerificationSummary returns a formatted summary of theme verification
117+func GetThemeVerificationSummary(result *ThemeVerification) string {
118+ var summary []string
119+
120+ if result.BleuThemeCloned {
121+ summary = append(summary, "[OK] Bleu-theme repository cloned")
122+ } else {
123+ summary = append(summary, "[WARN] Bleu-theme repository not cloned")
124+ }
125+
126+ // Theme files summary
127+ missingThemes := []string{}
128+ for tool, ok := range result.ThemeFilesOK {
129+ if !ok {
130+ missingThemes = append(missingThemes, tool)
131+ }
132+ }
133+ if len(missingThemes) > 0 {
134+ summary = append(summary, fmt.Sprintf("[WARN] Missing theme files: %s", strings.Join(missingThemes, ", ")))
135+ } else if len(result.ThemeFilesOK) > 0 {
136+ summary = append(summary, "[OK] All theme files found")
137+ }
138+
139+ // Config files summary
140+ missingConfigs := []string{}
141+ for tool, ok := range result.ConfigFilesOK {
142+ if !ok {
143+ missingConfigs = append(missingConfigs, tool)
144+ }
145+ }
146+ if len(missingConfigs) > 0 {
147+ summary = append(summary, fmt.Sprintf("[WARN] Missing config files: %s", strings.Join(missingConfigs, ", ")))
148+ } else if len(result.ConfigFilesOK) > 0 {
149+ summary = append(summary, "[OK] All config files found")
150+ }
151+
152+ // Bat cache
153+ if result.BatCacheBuilt {
154+ summary = append(summary, "[OK] Bat cache built successfully")
155+ }
156+
157+ // Starship config
158+ if result.StarshipValid {
159+ summary = append(summary, "[OK] Starship config valid")
160+ }
161+
162+ // Warnings
163+ for _, w := range result.Warnings {
164+ summary = append(summary, fmt.Sprintf("[WARN] %s", w))
165+ }
166+
167+ // Errors
168+ for _, e := range result.Errors {
169+ summary = append(summary, fmt.Sprintf("[ERROR] %s", e))
170+ }
171+
172+ return strings.Join(summary, "\n")
173+}
@@ -0,0 +1,173 @@
1+package verify
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+)
11+
12+// ThemeVerification holds results of theme verification
13+type ThemeVerification struct {
14+ BleuThemeCloned bool
15+ ThemeFilesOK map[string]bool // Tool name -> exists
16+ ConfigFilesOK map[string]bool // Tool name -> config file exists
17+ BatCacheBuilt bool
18+ StarshipValid bool
19+ Warnings []string
20+ Errors []string
21+}
22+
23+// NewThemeVerification creates a new theme verification result
24+func NewThemeVerification() *ThemeVerification {
25+ return &ThemeVerification{
26+ ThemeFilesOK: make(map[string]bool),
27+ ConfigFilesOK: make(map[string]bool),
28+ Warnings: []string{},
29+ Errors: []string{},
30+ }
31+}
32+
33+// ValidateBleuThemeClone verifies the bleu-theme repository was successfully cloned
34+func ValidateBleuThemeClone(fs interfaces.FileSystem, userHome string) bool {
35+ bleuThemeDir := filepath.Join(userHome, ".local", "share", "archup", "themes", "bleu")
36+ gitDir := filepath.Join(bleuThemeDir, ".git")
37+
38+ // Check if .git directory exists (indicates successful clone)
39+ _, err := fs.Stat(gitDir)
40+ return err == nil
41+}
42+
43+// ValidateThemeFiles checks if expected theme files exist after cloning
44+func ValidateThemeFiles(fs interfaces.FileSystem, userHome string) *ThemeVerification {
45+ result := NewThemeVerification()
46+ themeDir := filepath.Join(userHome, ".local", "share", "archup", "themes", "bleu")
47+
48+ // Check clone first
49+ result.BleuThemeCloned = ValidateBleuThemeClone(fs, userHome)
50+ if !result.BleuThemeCloned {
51+ result.Warnings = append(result.Warnings, "bleu-theme not cloned successfully")
52+ return result
53+ }
54+
55+ // Theme file paths to check
56+ themeFiles := map[string]string{
57+ "starship": filepath.Join(themeDir, "starship", "bleu.toml"),
58+ "eza": filepath.Join(themeDir, "eza", "theme.yml"),
59+ "bat": filepath.Join(themeDir, "bat", "bleu.tmTheme"),
60+ "btop": filepath.Join(themeDir, "btop", "bleu.theme"),
61+ "yazi": filepath.Join(themeDir, "yazi", "bleu.toml"),
62+ "fzf": filepath.Join(themeDir, "fzf", "bleu.sh"),
63+ }
64+
65+ for tool, path := range themeFiles {
66+ _, err := fs.Stat(path)
67+ exists := err == nil
68+ result.ThemeFilesOK[tool] = exists
69+ if !exists {
70+ result.Warnings = append(result.Warnings, fmt.Sprintf("%s theme file missing: %s", tool, path))
71+ }
72+ }
73+
74+ return result
75+}
76+
77+// ValidateConfigFiles checks if configuration files were successfully copied
78+func ValidateConfigFiles(fs interfaces.FileSystem, userHome string) *ThemeVerification {
79+ result := NewThemeVerification()
80+
81+ configFiles := map[string]string{
82+ "starship": filepath.Join(userHome, ".local", "share", "archup", "default", "starship.toml"),
83+ "eza": filepath.Join(userHome, ".config", "eza", "theme.yml"),
84+ "bat": filepath.Join(userHome, ".config", "bat", "themes", "bleu.tmTheme"),
85+ "btop": filepath.Join(userHome, ".config", "btop", "themes", "bleu.theme"),
86+ "yazi": filepath.Join(userHome, ".config", "yazi", "flavors", "bleu", "flavor.toml"),
87+ }
88+
89+ for tool, path := range configFiles {
90+ _, err := fs.Stat(path)
91+ exists := err == nil
92+ result.ConfigFilesOK[tool] = exists
93+ if !exists {
94+ result.Warnings = append(result.Warnings, fmt.Sprintf("%s config not found: %s", tool, path))
95+ }
96+ }
97+
98+ return result
99+}
100+
101+// ValidateBatCache checks if bat cache was built successfully
102+// This would require running `bat --list-themes` in chroot
103+func ValidateBatCache(chrExec interfaces.ChrootExecutor, logPath string) bool {
104+ cmd := "bat --list-themes | grep -i bleu"
105+ err := chrExec.ChrootExec(logPath, config.PathMnt, cmd)
106+ return err == nil
107+}
108+
109+// ValidateStarshipConfig tests if starship can load its configuration
110+func ValidateStarshipConfig(chrExec interfaces.ChrootExecutor, logPath string) bool {
111+ cmd := "starship config"
112+ err := chrExec.ChrootExec(logPath, config.PathMnt, cmd)
113+ return err == nil
114+}
115+
116+// GetThemeVerificationSummary returns a formatted summary of theme verification
117+func GetThemeVerificationSummary(result *ThemeVerification) string {
118+ var summary []string
119+
120+ if result.BleuThemeCloned {
121+ summary = append(summary, "[OK] Bleu-theme repository cloned")
122+ } else {
123+ summary = append(summary, "[WARN] Bleu-theme repository not cloned")
124+ }
125+
126+ // Theme files summary
127+ missingThemes := []string{}
128+ for tool, ok := range result.ThemeFilesOK {
129+ if !ok {
130+ missingThemes = append(missingThemes, tool)
131+ }
132+ }
133+ if len(missingThemes) > 0 {
134+ summary = append(summary, fmt.Sprintf("[WARN] Missing theme files: %s", strings.Join(missingThemes, ", ")))
135+ } else if len(result.ThemeFilesOK) > 0 {
136+ summary = append(summary, "[OK] All theme files found")
137+ }
138+
139+ // Config files summary
140+ missingConfigs := []string{}
141+ for tool, ok := range result.ConfigFilesOK {
142+ if !ok {
143+ missingConfigs = append(missingConfigs, tool)
144+ }
145+ }
146+ if len(missingConfigs) > 0 {
147+ summary = append(summary, fmt.Sprintf("[WARN] Missing config files: %s", strings.Join(missingConfigs, ", ")))
148+ } else if len(result.ConfigFilesOK) > 0 {
149+ summary = append(summary, "[OK] All config files found")
150+ }
151+
152+ // Bat cache
153+ if result.BatCacheBuilt {
154+ summary = append(summary, "[OK] Bat cache built successfully")
155+ }
156+
157+ // Starship config
158+ if result.StarshipValid {
159+ summary = append(summary, "[OK] Starship config valid")
160+ }
161+
162+ // Warnings
163+ for _, w := range result.Warnings {
164+ summary = append(summary, fmt.Sprintf("[WARN] %s", w))
165+ }
166+
167+ // Errors
168+ for _, e := range result.Errors {
169+ summary = append(summary, fmt.Sprintf("[ERROR] %s", e))
170+ }
171+
172+ return strings.Join(summary, "\n")
173+}

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