refactor: retire tui.BuiltinThemes in favor of theming.List
Make internal/theming.List the single source of truth for themes:
- Drop tui.BuiltinThemes, tui.ThemeByID, and the NvChadTheme field on
tui.Theme. tui.Theme is now a thin {ID, Name} display value.
- Wizard populates its dropdown from theming.List() and uses
DisplayName for the label.
- neovim.resolvedTheme uses theming.Get (with theming.List()[0] as a
fallback) instead of a caller-supplied []tui.Theme slice.
- neovim.New signature loses the themes []tui.Theme parameter; update
cmd/archduino/main.go and all neovim tests to match.
Break the resulting neovim→theming import cycle (theming previously
imported neovim for WriteChadrc) by inverting the dependency:
theming exposes a ChadrcWriter function hook, and neovim registers
its WriteChadrc into it via package init. The theming test stubs
ChadrcWriter directly to exercise the chadrc-rewrite branch without
pulling neovim in and re-creating the cycle.
Delete the now-obsolete TestBuiltinThemesCarryNvChadName / TestThemeByID*
tests in internal/tui/state_test.go — their coverage lives in the
theming package's own tests.
This commit is contained in:
@@ -57,7 +57,7 @@ func buildRegistry(cfg config.Config, themeID string) (*module.Registry, error)
|
|||||||
themes.New(cfg.Themes),
|
themes.New(cfg.Themes),
|
||||||
arduinocli.New(cfg.ArduinoCLI, runner),
|
arduinocli.New(cfg.ArduinoCLI, runner),
|
||||||
shellconfig.New(cfg.ShellConfig),
|
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),
|
renode.New(cfg.Renode, pm, yay),
|
||||||
}
|
}
|
||||||
for _, m := range mods {
|
for _, m := range mods {
|
||||||
|
|||||||
@@ -17,12 +17,19 @@ import (
|
|||||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
"github.com/MDietermann/ArduinoArchSetup/internal/theming"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/*
|
//go:embed assets/*
|
||||||
var assets embed.FS
|
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.
|
// assetMap lists each asset and its destination under ~/.config/nvim.
|
||||||
// Order matters: Detect iterates in this order and Apply writes in this order.
|
// Order matters: Detect iterates in this order and Apply writes in this order.
|
||||||
var assetMap = []struct {
|
var assetMap = []struct {
|
||||||
@@ -40,14 +47,13 @@ var assetMap = []struct {
|
|||||||
type Module struct {
|
type Module struct {
|
||||||
cfg config.NeovimConfig
|
cfg config.NeovimConfig
|
||||||
themeID string
|
themeID string
|
||||||
themes []tui.Theme
|
|
||||||
pm platform.PackageManager
|
pm platform.PackageManager
|
||||||
git platform.GitClient
|
git platform.GitClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a Neovim Module.
|
// New constructs a Neovim Module.
|
||||||
func New(cfg config.NeovimConfig, themeID string, themes []tui.Theme, pm platform.PackageManager, git platform.GitClient) Module {
|
func New(cfg config.NeovimConfig, themeID string, pm platform.PackageManager, git platform.GitClient) Module {
|
||||||
return Module{cfg: cfg, themeID: themeID, themes: themes, pm: pm, git: git}
|
return Module{cfg: cfg, themeID: themeID, pm: pm, git: git}
|
||||||
}
|
}
|
||||||
|
|
||||||
type state struct {
|
type state struct {
|
||||||
@@ -79,11 +85,11 @@ func nvimCfg() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Module) resolvedTheme() string {
|
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
|
return t.NvChadTheme
|
||||||
}
|
}
|
||||||
if len(m.themes) > 0 {
|
if list := theming.List(); len(list) > 0 && list[0].NvChadTheme != "" {
|
||||||
return m.themes[0].NvChadTheme
|
return list[0].NvChadTheme
|
||||||
}
|
}
|
||||||
return "gruvbox"
|
return "gruvbox"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeRunner struct {
|
type fakeRunner struct {
|
||||||
@@ -58,7 +57,7 @@ func TestDetectFreshSystem(t *testing.T) {
|
|||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
r := &fakeRunner{installed: map[string]bool{}}
|
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())
|
st, err := m.Detect(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -81,7 +80,7 @@ func TestDetectNonNvChadTriggersBackup(t *testing.T) {
|
|||||||
os.MkdirAll(filepath.Join(home, ".config", "nvim", "lua"), 0o755)
|
os.MkdirAll(filepath.Join(home, ".config", "nvim", "lua"), 0o755)
|
||||||
os.WriteFile(filepath.Join(home, ".config", "nvim", "lua", "plugins.lua"), []byte("-- lazyvim"), 0o644)
|
os.WriteFile(filepath.Join(home, ".config", "nvim", "lua", "plugins.lua"), []byte("-- lazyvim"), 0o644)
|
||||||
r := &fakeRunner{installed: map[string]bool{}}
|
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())
|
st, _ := m.Detect(context.Background())
|
||||||
if !st.(state).BackupNeeded {
|
if !st.(state).BackupNeeded {
|
||||||
t.Fatal("expected BackupNeeded=true")
|
t.Fatal("expected BackupNeeded=true")
|
||||||
@@ -92,7 +91,7 @@ func TestPlanApplyInstallsEverything(t *testing.T) {
|
|||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
r := &fakeRunner{installed: map[string]bool{}}
|
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())
|
st, _ := m.Detect(context.Background())
|
||||||
p, _ := m.Plan(context.Background(), st)
|
p, _ := m.Plan(context.Background(), st)
|
||||||
if len(p.Actions) == 0 {
|
if len(p.Actions) == 0 {
|
||||||
@@ -150,7 +149,7 @@ func TestPlanAfterApplyIsEmpty(t *testing.T) {
|
|||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
r := &fakeRunner{installed: map[string]bool{"neovim": true}}
|
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())
|
st, _ := m.Detect(context.Background())
|
||||||
p, _ := m.Plan(context.Background(), st)
|
p, _ := m.Plan(context.Background(), st)
|
||||||
_ = m.Apply(context.Background(), p, nopReporter{})
|
_ = m.Apply(context.Background(), p, nopReporter{})
|
||||||
@@ -165,14 +164,16 @@ func TestUnknownThemeFallsBackToFirst(t *testing.T) {
|
|||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
r := &fakeRunner{installed: map[string]bool{"neovim": true}}
|
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())
|
st, _ := m.Detect(context.Background())
|
||||||
p, _ := m.Plan(context.Background(), st)
|
p, _ := m.Plan(context.Background(), st)
|
||||||
if err := m.Apply(context.Background(), p, nopReporter{}); err != nil {
|
if err := m.Apply(context.Background(), p, nopReporter{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
chadrc, _ := os.ReadFile(filepath.Join(home, ".config", "nvim", "lua", "chadrc.lua"))
|
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)
|
t.Fatalf("fallback theme not applied: %s", chadrc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/neovim"
|
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||||
"gopkg.in/yaml.v3"
|
"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
|
//go:embed assets
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
@@ -146,8 +152,10 @@ func Apply(cfg config.Config, themeID string) error {
|
|||||||
return fmt.Errorf("update overlay: %w", err)
|
return fmt.Errorf("update overlay: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := neovim.WriteChadrc(th.NvChadTheme); err != nil {
|
if ChadrcWriter != nil {
|
||||||
return fmt.Errorf("update chadrc: %w", err)
|
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
|
// Clear NvChad's base46 theme cache so the new theme activates on next
|
||||||
|
|||||||
@@ -129,6 +129,17 @@ func TestApplyRewritesChadrcIfPresent(t *testing.T) {
|
|||||||
[]byte(`M.base46 = { theme = "onedark" }`), 0o644); err != nil {
|
[]byte(`M.base46 = { theme = "onedark" }`), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if err := Apply(config.Config{}, "gruvbox"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,35 +5,15 @@ import (
|
|||||||
|
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||||
|
"github.com/MDietermann/ArduinoArchSetup/internal/theming"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Theme is a named colorscheme presented in the wizard. Each entry carries
|
// Theme is a named colorscheme presented in the wizard. It is a thin
|
||||||
// per-tool theme names; Plan 3b uses NvChadTheme, later plans add more.
|
// display value for the dropdown; the authoritative theme catalogue lives
|
||||||
|
// in the theming package (theming.List / theming.Get).
|
||||||
type Theme struct {
|
type Theme struct {
|
||||||
ID string
|
ID string
|
||||||
Name 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleChoice is one row in the module selector.
|
// 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 })
|
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 != "" {
|
if override, ok := cfg.ThemeOverrides["__active"]; ok && override != "" {
|
||||||
theme = override
|
theme = override
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||||
|
"github.com/MDietermann/ArduinoArchSetup/internal/theming"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeModule struct{ id, name, desc string }
|
type fakeModule struct{ id, name, desc string }
|
||||||
@@ -34,7 +35,7 @@ func TestNewWizardStatePreTicksAllByDefault(t *testing.T) {
|
|||||||
t.Errorf("%s not selected", m.ID)
|
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)
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||||
|
"github.com/MDietermann/ArduinoArchSetup/internal/theming"
|
||||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
"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
|
return s, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
themeOptions := make([]huh.Option[string], 0, len(tui.BuiltinThemes))
|
themes := theming.List()
|
||||||
for _, t := range tui.BuiltinThemes {
|
themeOptions := make([]huh.Option[string], 0, len(themes))
|
||||||
themeOptions = append(themeOptions, huh.NewOption(t.Name, t.ID))
|
for _, t := range themes {
|
||||||
|
themeOptions = append(themeOptions, huh.NewOption(t.DisplayName, t.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
moduleOptions := make([]huh.Option[string], 0, len(s.Modules))
|
moduleOptions := make([]huh.Option[string], 0, len(s.Modules))
|
||||||
|
|||||||
Reference in New Issue
Block a user