From c0983284b3e3015aba1f57ce98de48ab224628f0 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 13 Apr 2026 21:49:14 +0200 Subject: [PATCH] feat(tui): add TeaReporter forwarding engine events to bubbletea --- internal/tui/reporter.go | 60 +++++++++++++++++++++++++++++++++++ internal/tui/reporter_test.go | 51 +++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 internal/tui/reporter.go create mode 100644 internal/tui/reporter_test.go diff --git a/internal/tui/reporter.go b/internal/tui/reporter.go new file mode 100644 index 0000000..38fd433 --- /dev/null +++ b/internal/tui/reporter.go @@ -0,0 +1,60 @@ +package tui + +import ( + "sync" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/MDietermann/ArduinoArchSetup/internal/module" +) + +// ProgressMsg is emitted when a module reports progress during Apply. +type ProgressMsg struct { + ModuleID string + Current int + Total int + Message string +} + +// LogMsg is emitted when a module emits a log line during Apply. +type LogMsg struct { + ModuleID string + Line string +} + +// sender abstracts tea.Program.Send so tests can inject a recorder. +type sender interface { + Send(msg tea.Msg) +} + +// TeaReporter implements module.Reporter and forwards every event to a +// sender (typically a *tea.Program). Concurrency-safe. +type TeaReporter struct { + mu sync.Mutex + s sender +} + +// NewTeaReporter wraps a sender. Passing nil is allowed — events are dropped. +func NewTeaReporter(s sender) *TeaReporter { + return &TeaReporter{s: s} +} + +func (r *TeaReporter) Progress(id string, current, total int, msg string) { + r.mu.Lock() + defer r.mu.Unlock() + if r.s == nil { + return + } + r.s.Send(ProgressMsg{ModuleID: id, Current: current, Total: total, Message: msg}) +} + +func (r *TeaReporter) Log(id, line string) { + r.mu.Lock() + defer r.mu.Unlock() + if r.s == nil { + return + } + r.s.Send(LogMsg{ModuleID: id, Line: line}) +} + +var _ module.Reporter = (*TeaReporter)(nil) diff --git a/internal/tui/reporter_test.go b/internal/tui/reporter_test.go new file mode 100644 index 0000000..1dfe0f0 --- /dev/null +++ b/internal/tui/reporter_test.go @@ -0,0 +1,51 @@ +package tui + +import ( + "sync" + "testing" + + tea "github.com/charmbracelet/bubbletea" +) + +type recorder struct { + mu sync.Mutex + msgs []tea.Msg +} + +func (r *recorder) Send(m tea.Msg) { + r.mu.Lock() + defer r.mu.Unlock() + r.msgs = append(r.msgs, m) +} + +func (r *recorder) get() []tea.Msg { + r.mu.Lock() + defer r.mu.Unlock() + out := make([]tea.Msg, len(r.msgs)) + copy(out, r.msgs) + return out +} + +func TestTeaReporterForwardsProgressAndLog(t *testing.T) { + rec := &recorder{} + r := NewTeaReporter(rec) + r.Progress("mod", 1, 4, "step one") + r.Log("mod", "hello world") + + msgs := rec.get() + if len(msgs) != 2 { + t.Fatalf("got %d msgs", len(msgs)) + } + if p, ok := msgs[0].(ProgressMsg); !ok || p.ModuleID != "mod" || p.Current != 1 || p.Total != 4 { + t.Errorf("msg[0] = %+v", msgs[0]) + } + if l, ok := msgs[1].(LogMsg); !ok || l.Line != "hello world" { + t.Errorf("msg[1] = %+v", msgs[1]) + } +} + +func TestTeaReporterNilSenderIsSafe(t *testing.T) { + r := NewTeaReporter(nil) + r.Progress("x", 1, 1, "y") // must not panic + r.Log("x", "y") +}