diff --git a/cmd/archduino/main.go b/cmd/archduino/main.go index bbcc835..a9eab4e 100644 --- a/cmd/archduino/main.go +++ b/cmd/archduino/main.go @@ -57,7 +57,7 @@ func buildRegistry(cfg config.Config, themeID string) (*module.Registry, error) themes.New(cfg.Themes), arduinocli.New(cfg.ArduinoCLI, runner), shellconfig.New(cfg.ShellConfig), - neovim.New(cfg.Neovim, themeID, tui.BuiltinThemes, pm, git), + neovim.New(cfg.Neovim, themeID, pm, git), renode.New(cfg.Renode, pm, yay), } for _, m := range mods { diff --git a/internal/modules/neovim/neovim.go b/internal/modules/neovim/neovim.go index 0f89cca..2a3f5a1 100644 --- a/internal/modules/neovim/neovim.go +++ b/internal/modules/neovim/neovim.go @@ -17,12 +17,19 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" "github.com/MDietermann/ArduinoArchSetup/internal/platform" - "github.com/MDietermann/ArduinoArchSetup/internal/tui" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" ) //go:embed assets/* var assets embed.FS +// Register WriteChadrc with the theming package so theming.Apply can +// rewrite chadrc.lua on theme changes without importing this package +// (which would be a cycle: neovim imports theming). +func init() { + theming.ChadrcWriter = WriteChadrc +} + // assetMap lists each asset and its destination under ~/.config/nvim. // Order matters: Detect iterates in this order and Apply writes in this order. var assetMap = []struct { @@ -40,14 +47,13 @@ var assetMap = []struct { type Module struct { cfg config.NeovimConfig themeID string - themes []tui.Theme pm platform.PackageManager git platform.GitClient } // New constructs a Neovim Module. -func New(cfg config.NeovimConfig, themeID string, themes []tui.Theme, pm platform.PackageManager, git platform.GitClient) Module { - return Module{cfg: cfg, themeID: themeID, themes: themes, pm: pm, git: git} +func New(cfg config.NeovimConfig, themeID string, pm platform.PackageManager, git platform.GitClient) Module { + return Module{cfg: cfg, themeID: themeID, pm: pm, git: git} } type state struct { @@ -79,11 +85,11 @@ func nvimCfg() (string, error) { } func (m Module) resolvedTheme() string { - if t, ok := tui.ThemeByID(m.themes, m.themeID); ok && t.NvChadTheme != "" { + if t, ok := theming.Get(m.themeID); ok && t.NvChadTheme != "" { return t.NvChadTheme } - if len(m.themes) > 0 { - return m.themes[0].NvChadTheme + if list := theming.List(); len(list) > 0 && list[0].NvChadTheme != "" { + return list[0].NvChadTheme } return "gruvbox" } diff --git a/internal/modules/neovim/neovim_test.go b/internal/modules/neovim/neovim_test.go index 862d107..bd43a96 100644 --- a/internal/modules/neovim/neovim_test.go +++ b/internal/modules/neovim/neovim_test.go @@ -10,7 +10,6 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" "github.com/MDietermann/ArduinoArchSetup/internal/platform" - "github.com/MDietermann/ArduinoArchSetup/internal/tui" ) type fakeRunner struct { @@ -58,7 +57,7 @@ func TestDetectFreshSystem(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", platform.NewPacman(r), platform.GitClient{Runner: r}) st, err := m.Detect(context.Background()) if err != nil { t.Fatal(err) @@ -81,7 +80,7 @@ func TestDetectNonNvChadTriggersBackup(t *testing.T) { os.MkdirAll(filepath.Join(home, ".config", "nvim", "lua"), 0o755) os.WriteFile(filepath.Join(home, ".config", "nvim", "lua", "plugins.lua"), []byte("-- lazyvim"), 0o644) r := &fakeRunner{installed: map[string]bool{}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) if !st.(state).BackupNeeded { t.Fatal("expected BackupNeeded=true") @@ -92,7 +91,7 @@ func TestPlanApplyInstallsEverything(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "dracula", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "dracula", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) if len(p.Actions) == 0 { @@ -150,7 +149,7 @@ func TestPlanAfterApplyIsEmpty(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{"neovim": true}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) _ = m.Apply(context.Background(), p, nopReporter{}) @@ -165,14 +164,16 @@ func TestUnknownThemeFallsBackToFirst(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{"neovim": true}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "does-not-exist", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "does-not-exist", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) if err := m.Apply(context.Background(), p, nopReporter{}); err != nil { t.Fatal(err) } chadrc, _ := os.ReadFile(filepath.Join(home, ".config", "nvim", "lua", "chadrc.lua")) - if !strings.Contains(string(chadrc), `theme = "gruvbox"`) { + // Unknown theme IDs fall back to the first entry of theming.List(), + // which is sorted alphabetically by ID (currently "catppuccin"). + if !strings.Contains(string(chadrc), `theme = "catppuccin"`) { t.Fatalf("fallback theme not applied: %s", chadrc) } } diff --git a/internal/theming/theming.go b/internal/theming/theming.go index cf5493a..904a2c1 100644 --- a/internal/theming/theming.go +++ b/internal/theming/theming.go @@ -13,11 +13,17 @@ import ( "strings" "github.com/MDietermann/ArduinoArchSetup/internal/config" - "github.com/MDietermann/ArduinoArchSetup/internal/modules/neovim" "github.com/MDietermann/ArduinoArchSetup/internal/platform" "gopkg.in/yaml.v3" ) +// ChadrcWriter is the hook used by Apply to rewrite NvChad's chadrc.lua +// after a theme change. The neovim module registers its WriteChadrc here +// via an init() function to avoid a theming→neovim import cycle (neovim +// now imports theming to look up the NvChad theme name). If nil, Apply +// skips the chadrc update silently. +var ChadrcWriter func(nvChadThemeName string) error + //go:embed assets var assetsFS embed.FS @@ -146,8 +152,10 @@ func Apply(cfg config.Config, themeID string) error { return fmt.Errorf("update overlay: %w", err) } - if err := neovim.WriteChadrc(th.NvChadTheme); err != nil { - return fmt.Errorf("update chadrc: %w", err) + if ChadrcWriter != nil { + if err := ChadrcWriter(th.NvChadTheme); err != nil { + return fmt.Errorf("update chadrc: %w", err) + } } // Clear NvChad's base46 theme cache so the new theme activates on next diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index b2bfc41..57785a4 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -129,6 +129,17 @@ func TestApplyRewritesChadrcIfPresent(t *testing.T) { []byte(`M.base46 = { theme = "onedark" }`), 0o644); err != nil { t.Fatal(err) } + + // The real ChadrcWriter lives in the neovim package, which cannot be + // imported here without creating an import cycle (neovim→theming). + // Stub it with a minimal writer that covers what the test asserts. + prev := ChadrcWriter + t.Cleanup(func() { ChadrcWriter = prev }) + ChadrcWriter = func(name string) error { + return os.WriteFile(filepath.Join(nvimCfg, "chadrc.lua"), + []byte(`theme = "`+name+`"`), 0o644) + } + if err := Apply(config.Config{}, "gruvbox"); err != nil { t.Fatal(err) } diff --git a/internal/tui/state.go b/internal/tui/state.go index 63b5965..a75380b 100644 --- a/internal/tui/state.go +++ b/internal/tui/state.go @@ -5,35 +5,15 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" ) -// Theme is a named colorscheme presented in the wizard. Each entry carries -// per-tool theme names; Plan 3b uses NvChadTheme, later plans add more. +// Theme is a named colorscheme presented in the wizard. It is a thin +// display value for the dropdown; the authoritative theme catalogue lives +// in the theming package (theming.List / theming.Get). type Theme struct { - ID string - Name string - NvChadTheme string -} - -// BuiltinThemes is the fixed list the wizard shows. Keep in sync with the -// themes shipped by Plan 3's theme module. -var BuiltinThemes = []Theme{ - {ID: "gruvbox", Name: "Gruvbox Dark", NvChadTheme: "gruvbox"}, - {ID: "catppuccin", Name: "Catppuccin Mocha", NvChadTheme: "catppuccin"}, - {ID: "tokyonight", Name: "Tokyo Night", NvChadTheme: "tokyonight"}, - {ID: "nord", Name: "Nord", NvChadTheme: "nord"}, - {ID: "dracula", Name: "Dracula", NvChadTheme: "dracula"}, -} - -// ThemeByID returns the Theme with the given ID. The bool is false when no -// match is found; callers decide whether to fall back or error. -func ThemeByID(themes []Theme, id string) (Theme, bool) { - for _, t := range themes { - if t.ID == id { - return t, true - } - } - return Theme{}, false + ID string + Name string } // ModuleChoice is one row in the module selector. @@ -74,7 +54,10 @@ func NewWizardState(cfg config.Config, reg *module.Registry) WizardState { } sort.Slice(choices, func(i, j int) bool { return choices[i].ID < choices[j].ID }) - theme := BuiltinThemes[0].ID + var theme string + if list := theming.List(); len(list) > 0 { + theme = list[0].ID + } if override, ok := cfg.ThemeOverrides["__active"]; ok && override != "" { theme = override } diff --git a/internal/tui/state_test.go b/internal/tui/state_test.go index 90d5dc7..c212492 100644 --- a/internal/tui/state_test.go +++ b/internal/tui/state_test.go @@ -6,6 +6,7 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" ) type fakeModule struct{ id, name, desc string } @@ -34,7 +35,7 @@ func TestNewWizardStatePreTicksAllByDefault(t *testing.T) { t.Errorf("%s not selected", m.ID) } } - if s.ThemeID != BuiltinThemes[0].ID { + if s.ThemeID != theming.List()[0].ID { t.Errorf("theme = %q", s.ThemeID) } } @@ -71,30 +72,3 @@ func TestSelectedIDsOnlyReturnsChecked(t *testing.T) { } } -func TestBuiltinThemesCarryNvChadName(t *testing.T) { - for _, th := range BuiltinThemes { - if th.NvChadTheme == "" { - t.Errorf("theme %q has empty NvChadTheme", th.ID) - } - } -} - -func TestThemeByIDReturnsMatch(t *testing.T) { - got, ok := ThemeByID(BuiltinThemes, "dracula") - if !ok { - t.Fatal("dracula should exist") - } - if got.ID != "dracula" { - t.Fatalf("got id=%q", got.ID) - } - if got.NvChadTheme == "" { - t.Fatal("NvChadTheme empty") - } -} - -func TestThemeByIDFallsBackWhenUnknown(t *testing.T) { - _, ok := ThemeByID(BuiltinThemes, "does-not-exist") - if ok { - t.Fatal("unknown id should report ok=false") - } -} diff --git a/internal/tui/wizard/wizard.go b/internal/tui/wizard/wizard.go index a3ec35a..77ad965 100644 --- a/internal/tui/wizard/wizard.go +++ b/internal/tui/wizard/wizard.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/huh" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" "github.com/MDietermann/ArduinoArchSetup/internal/tui" ) @@ -23,9 +24,10 @@ func Run(ctx context.Context, reg *module.Registry, initial tui.WizardState) (tu return s, false, nil } - themeOptions := make([]huh.Option[string], 0, len(tui.BuiltinThemes)) - for _, t := range tui.BuiltinThemes { - themeOptions = append(themeOptions, huh.NewOption(t.Name, t.ID)) + themes := theming.List() + themeOptions := make([]huh.Option[string], 0, len(themes)) + for _, t := range themes { + themeOptions = append(themeOptions, huh.NewOption(t.DisplayName, t.ID)) } moduleOptions := make([]huh.Option[string], 0, len(s.Modules))