Files
Archduino/internal/tui/dashboard/model.go

179 lines
4.4 KiB
Go

package dashboard
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
)
// DoneMsg is sent from the engine goroutine once engine.Run returns.
type DoneMsg struct {
Summary engine.Summary
Err error
}
// rowState tracks per-module progress in the dashboard.
// Name, Started, and Ended are populated for future use by elapsed-time
// rendering (planned for a later iteration); they are currently not read by View.
type rowState struct {
ID string
Name string
Current int
Total int
LastMsg string
LogTail []string
StatusIcon string // ·, ⠋ (running), ✓, ✗, ⊘
Done bool
Started time.Time
Ended time.Time
Err error
}
// Model is the Bubble Tea model for the Apply-phase dashboard.
type Model struct {
rows []*rowState
rowIndex map[string]*rowState
cursor int
showLogs bool
summary *engine.Summary
runErr error
}
// NewModel seeds the dashboard from the list of module IDs selected to run.
// Rows start in the "pending" state and flip to running on first ProgressMsg.
func NewModel(selected []string, nameByID map[string]string) *Model {
m := &Model{rowIndex: make(map[string]*rowState, len(selected))}
for _, id := range selected {
name := id
if n, ok := nameByID[id]; ok {
name = n
}
r := &rowState{ID: id, Name: name, StatusIcon: "·"}
m.rows = append(m.rows, r)
m.rowIndex[id] = r
}
return m
}
func (m *Model) Init() tea.Cmd { return nil }
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tui.ProgressMsg:
// Guard: ignore late Progress events that arrive after DoneMsg has
// finalised this row. tea.Program.Send is asynchronous and concurrent
// goroutines have no arrival-order guarantee.
if r, ok := m.rowIndex[msg.ModuleID]; ok && !r.Done {
if r.Started.IsZero() {
r.Started = time.Now()
}
r.Current = msg.Current
r.Total = msg.Total
r.LastMsg = msg.Message
r.StatusIcon = "⠋"
}
case tui.LogMsg:
if r, ok := m.rowIndex[msg.ModuleID]; ok && !r.Done {
r.LogTail = appendCapped(r.LogTail, msg.Line, 200)
}
case DoneMsg:
m.summary = &msg.Summary
m.runErr = msg.Err
for _, res := range msg.Summary.Results {
r, ok := m.rowIndex[res.ID]
if !ok {
continue
}
r.Done = true
r.Ended = time.Now()
switch res.Status {
case engine.StatusSucceeded, engine.StatusPlanned:
r.StatusIcon = "✓"
case engine.StatusFailed:
r.StatusIcon = "✗"
r.Err = res.Err
case engine.StatusSkipped:
r.StatusIcon = "⊘"
}
}
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.rows)-1 {
m.cursor++
}
case "enter":
m.showLogs = !m.showLogs
}
}
return m, nil
}
func (m *Model) View() string {
var b strings.Builder
header := lipgloss.NewStyle().Bold(true).Render("archduino install")
b.WriteString(header + "\n")
for i, r := range m.rows {
cursor := " "
if i == m.cursor {
cursor = "▸ "
}
progress := ""
if r.Total > 0 {
progress = fmt.Sprintf("%d/%d", r.Current, r.Total)
}
detail := r.LastMsg
if r.Err != nil {
detail = "failed — " + r.Err.Error()
}
fmt.Fprintf(&b, "%s%s %-16s %-6s %s\n", cursor, r.StatusIcon, r.ID, progress, detail)
}
if m.showLogs && len(m.rows) > 0 {
active := m.rows[m.cursor]
b.WriteString("\n─ log tail ─\n")
for _, line := range active.LogTail {
b.WriteString(" " + line + "\n")
}
}
if m.summary != nil {
ok, fail, skip := m.summary.Counts()
fmt.Fprintf(&b, "\nsummary: %d succeeded, %d failed, %d skipped\n", ok, fail, skip)
if m.runErr != nil {
fmt.Fprintf(&b, "error: %v\n", m.runErr)
}
b.WriteString("\n[q] quit\n")
} else {
b.WriteString("\n[↑/↓] navigate [enter] toggle log [q] abort\n")
}
return b.String()
}
// Summary returns the engine summary once the run has completed, or nil.
func (m *Model) Summary() *engine.Summary { return m.summary }
// RunErr returns the engine's top-level error, if any.
func (m *Model) RunErr() error { return m.runErr }
func appendCapped(s []string, line string, max int) []string {
s = append(s, line)
if len(s) > max {
s = s[len(s)-max:]
}
return s
}