From 7baf0fceaa6d2238f1f300a9ef4154505bbdac44 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 13 Apr 2026 22:11:54 +0200 Subject: [PATCH] feat(tui): add dashboard.Run bridging engine to Bubble Tea --- internal/tui/dashboard/run.go | 46 ++++++++++++++++++++++++++++++ internal/tui/dashboard/run_test.go | 43 ++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 internal/tui/dashboard/run.go create mode 100644 internal/tui/dashboard/run_test.go diff --git a/internal/tui/dashboard/run.go b/internal/tui/dashboard/run.go new file mode 100644 index 0000000..94e4521 --- /dev/null +++ b/internal/tui/dashboard/run.go @@ -0,0 +1,46 @@ +package dashboard + +import ( + "context" + "fmt" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/MDietermann/ArduinoArchSetup/internal/engine" + "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/tui" +) + +// Run displays the dashboard while engine.Run executes the selected modules. +// Returns the engine summary (for writeState) and the top-level error, if any. +func Run(ctx context.Context, reg *module.Registry, state tui.WizardState, halt bool) (engine.Summary, error) { + selected := state.SelectedIDs() + nameByID := make(map[string]string, len(state.Modules)) + for _, m := range state.Modules { + nameByID[m.ID] = m.Name + } + + m := NewModel(selected, nameByID) + p := tea.NewProgram(m, tea.WithContext(ctx)) + + reporter := tui.NewTeaReporter(p) + + go func() { + summary, err := engine.Run(ctx, reg, engine.RunOptions{ + Selected: selected, + HaltOnError: halt, + Reporter: reporter, + }) + p.Send(DoneMsg{Summary: summary, Err: err}) + }() + + if _, err := p.Run(); err != nil { + return engine.Summary{}, fmt.Errorf("tui run: %w", err) + } + + var sum engine.Summary + if m.Summary() != nil { + sum = *m.Summary() + } + return sum, m.RunErr() +} diff --git a/internal/tui/dashboard/run_test.go b/internal/tui/dashboard/run_test.go new file mode 100644 index 0000000..71b45fe --- /dev/null +++ b/internal/tui/dashboard/run_test.go @@ -0,0 +1,43 @@ +package dashboard + +import ( + "bytes" + "io" + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" + + "github.com/MDietermann/ArduinoArchSetup/internal/engine" + "github.com/MDietermann/ArduinoArchSetup/internal/tui" +) + +// TestModelEndToEnd drives the Model through teatest, feeding in the same +// message sequence that the TeaReporter + engine goroutine would produce. +func TestModelEndToEnd(t *testing.T) { + m := NewModel([]string{"alpha"}, map[string]string{"alpha": "Alpha"}) + tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(80, 24)) + + tm.Send(tui.ProgressMsg{ModuleID: "alpha", Current: 1, Total: 1, Message: "installing"}) + tm.Send(DoneMsg{Summary: engine.Summary{Results: []engine.ModuleResult{ + {ID: "alpha", Status: engine.StatusSucceeded}, + }}}) + tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) + + tm.WaitFinished(t, teatest.WithFinalTimeout(3*time.Second)) + + out := readAll(t, tm.FinalOutput(t)) + if !bytes.Contains(out, []byte("alpha")) || !bytes.Contains(out, []byte("summary:")) { + t.Fatalf("unexpected final output:\n%s", out) + } +} + +func readAll(t *testing.T, r io.Reader) []byte { + t.Helper() + b, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + return b +}