284 lines
7.0 KiB
Go
284 lines
7.0 KiB
Go
package ui
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/Ullaakut/cameradar/v6"
|
|
"github.com/charmbracelet/bubbles/progress"
|
|
"github.com/charmbracelet/bubbles/spinner"
|
|
"github.com/charmbracelet/bubbles/table"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type modelState struct {
|
|
steps []cameradar.Step
|
|
status map[cameradar.Step]state
|
|
logs []logMsg
|
|
summaryStreams []cameradar.Stream
|
|
summaryFinal bool
|
|
buildInfo BuildInfo
|
|
cancel context.CancelFunc
|
|
debug bool
|
|
spinner spinner.Model
|
|
progress progress.Model
|
|
width int
|
|
height int
|
|
quitting bool
|
|
progressTotals map[cameradar.Step]int
|
|
progressCounts map[cameradar.Step]int
|
|
progressTarget float64
|
|
progressVisible float64
|
|
}
|
|
|
|
func (m *modelState) Init() tea.Cmd {
|
|
return m.spinner.Tick
|
|
}
|
|
|
|
func (m *modelState) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var cmds []tea.Cmd
|
|
|
|
switch typed := msg.(type) {
|
|
case stepMsg:
|
|
m.handleStepMsg(typed)
|
|
case logMsg:
|
|
m.handleLogMsg(typed)
|
|
case summaryMsg:
|
|
m.handleSummaryMsg(typed)
|
|
case progressMsg:
|
|
m.handleProgressMsg(typed)
|
|
case closeMsg:
|
|
m.quitting = true
|
|
case tea.KeyMsg:
|
|
if typed.Type == tea.KeyCtrlC {
|
|
if m.cancel != nil {
|
|
m.cancel()
|
|
}
|
|
m.quitting = true
|
|
return m, tea.Quit
|
|
}
|
|
case spinner.TickMsg:
|
|
cmds = m.handleSpinnerMsg(typed)
|
|
case tea.WindowSizeMsg:
|
|
m.handleWindowSizeMsg(typed)
|
|
case progress.FrameMsg:
|
|
}
|
|
|
|
if len(cmds) == 0 {
|
|
return m, nil
|
|
}
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (m *modelState) handleStepMsg(msg stepMsg) {
|
|
m.status[msg.step] = msg.state
|
|
if msg.message != "" {
|
|
level := logInfo
|
|
if msg.state == stateError {
|
|
level = logError
|
|
}
|
|
m.logs = append(m.logs, logMsg{level: level, step: msg.step, message: msg.message})
|
|
}
|
|
if msg.state == stateDone || msg.state == stateError {
|
|
markStepComplete(m, msg.step)
|
|
queueProgressUpdate(m)
|
|
}
|
|
}
|
|
|
|
func (m *modelState) handleLogMsg(msg logMsg) {
|
|
m.logs = append(m.logs, msg)
|
|
}
|
|
|
|
func (m *modelState) handleSummaryMsg(msg summaryMsg) {
|
|
m.summaryStreams = msg.streams
|
|
m.summaryFinal = msg.final
|
|
if msg.final {
|
|
m.status[cameradar.StepSummary] = stateDone
|
|
markStepComplete(m, cameradar.StepSummary)
|
|
queueProgressUpdate(m)
|
|
m.quitting = true
|
|
}
|
|
}
|
|
|
|
func (m *modelState) handleProgressMsg(msg progressMsg) {
|
|
if msg.total > 0 {
|
|
m.progressTotals[msg.step] = msg.total
|
|
if m.progressCounts[msg.step] > msg.total {
|
|
m.progressCounts[msg.step] = msg.total
|
|
}
|
|
}
|
|
|
|
if msg.increment > 0 {
|
|
m.progressCounts[msg.step] += msg.increment
|
|
total := m.progressTotals[msg.step]
|
|
if total > 0 && m.progressCounts[msg.step] > total {
|
|
m.progressCounts[msg.step] = total
|
|
}
|
|
}
|
|
|
|
queueProgressUpdate(m)
|
|
}
|
|
|
|
func (m *modelState) handleSpinnerMsg(msg spinner.TickMsg) []tea.Cmd {
|
|
var cmds []tea.Cmd
|
|
var cmd tea.Cmd
|
|
m.spinner, cmd = m.spinner.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
advanceProgress(m)
|
|
if m.quitting && progressComplete(*m) {
|
|
cmds = append(cmds, tea.Quit)
|
|
}
|
|
return cmds
|
|
}
|
|
|
|
func (m *modelState) handleWindowSizeMsg(msg tea.WindowSizeMsg) {
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
m.progress.Width = progressWidth(msg.Width)
|
|
}
|
|
|
|
func (m *modelState) View() string {
|
|
var builder strings.Builder
|
|
header := sectionStyle.Render(m.buildInfo.TUIHeader())
|
|
headerLines := splitLines(header)
|
|
builder.WriteString(strings.Join(headerLines, "\n"))
|
|
builder.WriteString("\n\n")
|
|
|
|
stepsLines := m.renderSteps()
|
|
builder.WriteString(strings.Join(stepsLines, "\n"))
|
|
builder.WriteString("\n\n")
|
|
|
|
summaryHeight, logsHeight := m.layoutHeights(len(headerLines), len(stepsLines))
|
|
logsLines := m.renderLogs(logsHeight)
|
|
builder.WriteString(sectionStyle.Render("Logs"))
|
|
builder.WriteString("\n")
|
|
builder.WriteString(strings.Join(logsLines, "\n"))
|
|
builder.WriteString("\n\n")
|
|
|
|
rowsToShow := max(1, summaryHeight-2)
|
|
summaryTitle := renderSummaryTitle(m.summaryStreams)
|
|
summaryTables := buildSummaryTables(m.summaryStreams, m.width, m.status, rowsToShow)
|
|
builder.WriteString(sectionStyle.Render(summaryTitle))
|
|
builder.WriteString("\n")
|
|
for i, summary := range summaryTables {
|
|
builder.WriteString(summaryTableStyle.Render(summary.table.View()))
|
|
if i < len(summaryTables)-1 {
|
|
builder.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
func (m *modelState) FinalView() string {
|
|
var builder strings.Builder
|
|
header := sectionStyle.Render(m.buildInfo.TUIHeader())
|
|
headerLines := splitLines(header)
|
|
builder.WriteString(strings.Join(headerLines, "\n"))
|
|
builder.WriteString("\n\n")
|
|
|
|
stepsLines := m.renderSteps()
|
|
builder.WriteString(strings.Join(stepsLines, "\n"))
|
|
builder.WriteString("\n\n")
|
|
|
|
builder.WriteString(sectionStyle.Render("Logs"))
|
|
builder.WriteString("\n")
|
|
logLines := m.renderLogsAll()
|
|
if len(logLines) == 0 {
|
|
builder.WriteString(dimStyle.Render("No events yet."))
|
|
} else {
|
|
builder.WriteString(strings.Join(logLines, "\n"))
|
|
}
|
|
builder.WriteString("\n\n")
|
|
|
|
summaryTitle := renderSummaryTitle(m.summaryStreams)
|
|
visibility := summaryVisibility(summaryStatusAllDone())
|
|
accessible, others := partitionStreams(m.summaryStreams)
|
|
rows := append(buildSummaryRows(accessible, visibility), buildSummaryRows(others, visibility)...)
|
|
if len(rows) == 0 {
|
|
rows = []table.Row{emptySummaryRow()}
|
|
}
|
|
columns := summaryColumns(m.width, rows)
|
|
builder.WriteString(sectionStyle.Render(summaryTitle))
|
|
builder.WriteString("\n")
|
|
builder.WriteString(renderSummaryTablePlain(columns, rows))
|
|
return builder.String()
|
|
}
|
|
|
|
func (m *modelState) renderSteps() []string {
|
|
lines := []string{sectionStyle.Render("Steps"), renderProgress(m)}
|
|
spinnerView := m.spinner.View()
|
|
for _, step := range m.steps {
|
|
lines = append(lines, renderStep(step, m.status[step], spinnerView))
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func (m *modelState) renderLogs(height int) []string {
|
|
if height <= 0 {
|
|
return nil
|
|
}
|
|
if len(m.logs) == 0 {
|
|
lines := []string{dimStyle.Render("No events yet.")}
|
|
return padLines(lines, height)
|
|
}
|
|
|
|
start := 0
|
|
if len(m.logs) > height {
|
|
start = len(m.logs) - height
|
|
}
|
|
lines := make([]string, 0, min(height, len(m.logs)))
|
|
for _, entry := range m.logs[start:] {
|
|
lines = append(lines, renderLog(entry))
|
|
}
|
|
return padLines(lines, height)
|
|
}
|
|
|
|
func (m *modelState) renderLogsAll() []string {
|
|
if len(m.logs) == 0 {
|
|
return nil
|
|
}
|
|
lines := make([]string, 0, len(m.logs))
|
|
for _, entry := range m.logs {
|
|
lines = append(lines, renderLog(entry))
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func (m *modelState) layoutHeights(headerLines, stepsLines int) (summaryHeight, logsHeight int) {
|
|
if m.height <= 0 {
|
|
return summaryMinHeight, len(m.logs)
|
|
}
|
|
|
|
reserved := headerLines + 1 + stepsLines + 1 + 1 + 1
|
|
remaining := m.height - reserved
|
|
remaining = max(0, remaining)
|
|
|
|
switch {
|
|
case remaining < summaryMinHeight:
|
|
summaryHeight = max(3, remaining)
|
|
case remaining > summaryMaxHeight:
|
|
summaryHeight = summaryMaxHeight
|
|
default:
|
|
summaryHeight = remaining
|
|
}
|
|
|
|
logsHeight = max(0, remaining-summaryHeight)
|
|
|
|
return summaryHeight, logsHeight
|
|
}
|
|
|
|
func padLines(lines []string, height int) []string {
|
|
if height <= 0 {
|
|
return lines
|
|
}
|
|
for len(lines) < height {
|
|
lines = append(lines, "")
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func splitLines(value string) []string {
|
|
return strings.Split(value, "\n")
|
|
}
|