From 258b53b6a0429e4867c03384a5ffa77b9b621391 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 13 Apr 2026 21:48:12 +0200 Subject: [PATCH] feat(tui): add shared WizardState seeded from config and registry --- internal/tui/state.go | 86 ++++++++++++++++++++++++++++++++++++++ internal/tui/state_test.go | 72 +++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 internal/tui/state.go create mode 100644 internal/tui/state_test.go diff --git a/internal/tui/state.go b/internal/tui/state.go new file mode 100644 index 0000000..5120f51 --- /dev/null +++ b/internal/tui/state.go @@ -0,0 +1,86 @@ +package tui + +import ( + "sort" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/module" +) + +// Theme is a named colorscheme presented in the wizard. Plan 3 wires these +// names to actual theme modules; for now they are display-only strings. +type Theme struct { + ID string + Name 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"}, + {ID: "catppuccin", Name: "Catppuccin Mocha"}, + {ID: "tokyonight", Name: "Tokyo Night"}, + {ID: "nord", Name: "Nord"}, + {ID: "dracula", Name: "Dracula"}, +} + +// ModuleChoice is one row in the module selector. +type ModuleChoice struct { + ID string + Name string + Description string + Selected bool +} + +// WizardState is the payload the wizard fills in and the dashboard reads. +type WizardState struct { + ThemeID string + Modules []ModuleChoice + Parallelism int // advanced; reserved for a future concurrent engine + HaltOnError bool // advanced + Confirmed bool // true once the user hits Confirm on the last screen +} + +// NewWizardState builds an initial state from the running config and the +// registered modules. Pre-ticks all modules except those disabled in the +// overlay, defaults the theme to the first built-in theme. +func NewWizardState(cfg config.Config, reg *module.Registry) WizardState { + disabled := map[string]bool{} + for _, id := range cfg.DisabledModules { + disabled[id] = true + } + mods := reg.All() + choices := make([]ModuleChoice, 0, len(mods)) + for _, m := range mods { + meta := m.Meta() + choices = append(choices, ModuleChoice{ + ID: meta.ID, + Name: meta.Name, + Description: meta.Description, + Selected: !disabled[meta.ID], + }) + } + sort.Slice(choices, func(i, j int) bool { return choices[i].ID < choices[j].ID }) + + theme := BuiltinThemes[0].ID + if override, ok := cfg.ThemeOverrides["__active"]; ok && override != "" { + theme = override + } + + return WizardState{ + ThemeID: theme, + Modules: choices, + Parallelism: 1, + } +} + +// SelectedIDs returns only the checked module IDs. +func (s WizardState) SelectedIDs() []string { + out := make([]string, 0, len(s.Modules)) + for _, m := range s.Modules { + if m.Selected { + out = append(out, m.ID) + } + } + return out +} diff --git a/internal/tui/state_test.go b/internal/tui/state_test.go new file mode 100644 index 0000000..192272d --- /dev/null +++ b/internal/tui/state_test.go @@ -0,0 +1,72 @@ +package tui + +import ( + "context" + "testing" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/module" +) + +type fakeModule struct{ id, name, desc string } + +func (f fakeModule) Meta() module.Meta { + return module.Meta{ID: f.id, Name: f.name, Description: f.desc} +} +func (f fakeModule) Detect(context.Context) (module.State, error) { return nil, nil } +func (f fakeModule) Plan(context.Context, module.State) (module.Plan, error) { + return module.Plan{}, nil +} +func (f fakeModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil } +func (f fakeModule) Verify(context.Context) error { return nil } + +func TestNewWizardStatePreTicksAllByDefault(t *testing.T) { + reg := module.NewRegistry() + _ = reg.Register(fakeModule{id: "a", name: "Alpha"}) + _ = reg.Register(fakeModule{id: "b", name: "Bravo"}) + + s := NewWizardState(config.Config{}, reg) + if len(s.Modules) != 2 { + t.Fatalf("modules = %d", len(s.Modules)) + } + for _, m := range s.Modules { + if !m.Selected { + t.Errorf("%s not selected", m.ID) + } + } + if s.ThemeID != BuiltinThemes[0].ID { + t.Errorf("theme = %q", s.ThemeID) + } +} + +func TestNewWizardStateRespectsDisabled(t *testing.T) { + reg := module.NewRegistry() + _ = reg.Register(fakeModule{id: "a"}) + _ = reg.Register(fakeModule{id: "b"}) + + s := NewWizardState(config.Config{DisabledModules: []string{"a"}}, reg) + for _, m := range s.Modules { + switch m.ID { + case "a": + if m.Selected { + t.Error("a should be unticked") + } + case "b": + if !m.Selected { + t.Error("b should be ticked") + } + } + } +} + +func TestSelectedIDsOnlyReturnsChecked(t *testing.T) { + s := WizardState{Modules: []ModuleChoice{ + {ID: "a", Selected: true}, + {ID: "b", Selected: false}, + {ID: "c", Selected: true}, + }} + got := s.SelectedIDs() + if len(got) != 2 || got[0] != "a" || got[1] != "c" { + t.Fatalf("got %v", got) + } +}