feat: tui mode improvements (#395)
This commit is contained in:
committed by
GitHub
parent
d16443109a
commit
c11e3217ea
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.5 MiB |
+133
-38
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/Ullaakut/cameradar/v6"
|
"github.com/Ullaakut/cameradar/v6"
|
||||||
"github.com/charmbracelet/bubbles/progress"
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@ type modelState struct {
|
|||||||
steps []cameradar.Step
|
steps []cameradar.Step
|
||||||
status map[cameradar.Step]state
|
status map[cameradar.Step]state
|
||||||
logs []logMsg
|
logs []logMsg
|
||||||
summary []summaryTable
|
|
||||||
summaryStreams []cameradar.Stream
|
summaryStreams []cameradar.Stream
|
||||||
summaryFinal bool
|
summaryFinal bool
|
||||||
buildInfo BuildInfo
|
buildInfo BuildInfo
|
||||||
@@ -23,6 +23,7 @@ type modelState struct {
|
|||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
progress progress.Model
|
progress progress.Model
|
||||||
width int
|
width int
|
||||||
|
height int
|
||||||
quitting bool
|
quitting bool
|
||||||
progressTotals map[cameradar.Step]int
|
progressTotals map[cameradar.Step]int
|
||||||
progressCounts map[cameradar.Step]int
|
progressCounts map[cameradar.Step]int
|
||||||
@@ -82,7 +83,6 @@ func (m *modelState) handleStepMsg(msg stepMsg) {
|
|||||||
markStepComplete(m, msg.step)
|
markStepComplete(m, msg.step)
|
||||||
queueProgressUpdate(m)
|
queueProgressUpdate(m)
|
||||||
}
|
}
|
||||||
m.summary = buildSummaryTables(m.summaryStreams, m.width, m.status, m.summaryFinal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelState) handleLogMsg(msg logMsg) {
|
func (m *modelState) handleLogMsg(msg logMsg) {
|
||||||
@@ -92,7 +92,6 @@ func (m *modelState) handleLogMsg(msg logMsg) {
|
|||||||
func (m *modelState) handleSummaryMsg(msg summaryMsg) {
|
func (m *modelState) handleSummaryMsg(msg summaryMsg) {
|
||||||
m.summaryStreams = msg.streams
|
m.summaryStreams = msg.streams
|
||||||
m.summaryFinal = msg.final
|
m.summaryFinal = msg.final
|
||||||
m.summary = buildSummaryTables(msg.streams, m.width, m.status, msg.final)
|
|
||||||
if msg.final {
|
if msg.final {
|
||||||
m.status[cameradar.StepSummary] = stateDone
|
m.status[cameradar.StepSummary] = stateDone
|
||||||
markStepComplete(m, cameradar.StepSummary)
|
markStepComplete(m, cameradar.StepSummary)
|
||||||
@@ -134,55 +133,151 @@ func (m *modelState) handleSpinnerMsg(msg spinner.TickMsg) []tea.Cmd {
|
|||||||
|
|
||||||
func (m *modelState) handleWindowSizeMsg(msg tea.WindowSizeMsg) {
|
func (m *modelState) handleWindowSizeMsg(msg tea.WindowSizeMsg) {
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
m.progress.Width = progressWidth(msg.Width)
|
m.progress.Width = progressWidth(msg.Width)
|
||||||
m.summary = buildSummaryTables(m.summaryStreams, m.width, m.status, m.summaryFinal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelState) View() string {
|
func (m *modelState) View() string {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString(sectionStyle.Render(m.buildInfo.TUIHeader()))
|
header := sectionStyle.Render(m.buildInfo.TUIHeader())
|
||||||
builder.WriteString("\n")
|
headerLines := splitLines(header)
|
||||||
builder.WriteString(renderProgress(m))
|
builder.WriteString(strings.Join(headerLines, "\n"))
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n\n")
|
||||||
|
|
||||||
spinnerView := m.spinner.View()
|
stepsLines := m.renderSteps()
|
||||||
for _, step := range m.steps {
|
builder.WriteString(strings.Join(stepsLines, "\n"))
|
||||||
builder.WriteString(renderStep(step, m.status[step], spinnerView))
|
builder.WriteString("\n\n")
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString("\n")
|
summaryHeight, logsHeight := m.layoutHeights(len(headerLines), len(stepsLines))
|
||||||
|
logsLines := m.renderLogs(logsHeight)
|
||||||
builder.WriteString(sectionStyle.Render("Logs"))
|
builder.WriteString(sectionStyle.Render("Logs"))
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
if len(m.logs) == 0 {
|
builder.WriteString(strings.Join(logsLines, "\n"))
|
||||||
builder.WriteString(dimStyle.Render("No events yet."))
|
builder.WriteString("\n\n")
|
||||||
builder.WriteString("\n")
|
|
||||||
} else {
|
|
||||||
for _, entry := range m.logs {
|
|
||||||
builder.WriteString(renderLog(entry))
|
|
||||||
builder.WriteString("\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")
|
builder.WriteString("\n")
|
||||||
builder.WriteString(sectionStyle.Render("Summary"))
|
for i, summary := range summaryTables {
|
||||||
builder.WriteString("\n")
|
builder.WriteString(summaryTableStyle.Render(summary.table.View()))
|
||||||
for i, summary := range m.summary {
|
if i < len(summaryTables)-1 {
|
||||||
if summary.title != "" {
|
|
||||||
builder.WriteString(subsectionStyle.Render(summary.title))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
if summary.emptyMessage != "" {
|
|
||||||
builder.WriteString(dimStyle.Render(summary.emptyMessage))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
} else {
|
|
||||||
builder.WriteString(summaryTableStyle.Render(summary.table.View()))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
if i < len(m.summary)-1 {
|
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.String()
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import "github.com/charmbracelet/lipgloss"
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
sectionStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63"))
|
sectionStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63"))
|
||||||
subsectionStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("111"))
|
|
||||||
infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||||
debugStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
debugStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
||||||
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
|
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
|
||||||
|
|||||||
+169
-16
@@ -59,17 +59,23 @@ type summaryMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type summaryTable struct {
|
type summaryTable struct {
|
||||||
title string
|
table table.Model
|
||||||
table table.Model
|
|
||||||
emptyMessage string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
summaryMinHeight = 8
|
||||||
|
summaryMaxHeight = 10
|
||||||
|
summaryColumnCount = 8
|
||||||
|
)
|
||||||
|
|
||||||
// TUIReporter renders a Bubble Tea based UI.
|
// TUIReporter renders a Bubble Tea based UI.
|
||||||
type TUIReporter struct {
|
type TUIReporter struct {
|
||||||
program *tea.Program
|
program *tea.Program
|
||||||
debug bool
|
debug bool
|
||||||
once sync.Once
|
once sync.Once
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
|
mu sync.Mutex
|
||||||
|
last []cameradar.Stream
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTUIReporter creates a new Bubble Tea reporter.
|
// NewTUIReporter creates a new Bubble Tea reporter.
|
||||||
@@ -96,7 +102,6 @@ func NewTUIReporter(debug bool, out io.Writer, buildInfo BuildInfo, cancel conte
|
|||||||
progressTotals: make(map[cameradar.Step]int),
|
progressTotals: make(map[cameradar.Step]int),
|
||||||
progressCounts: make(map[cameradar.Step]int),
|
progressCounts: make(map[cameradar.Step]int),
|
||||||
}
|
}
|
||||||
initial.summary = buildSummaryTables(nil, initial.width, initial.status, false)
|
|
||||||
|
|
||||||
p := tea.NewProgram(initial, tea.WithInputTTY(), tea.WithOutput(out), tea.WithAltScreen())
|
p := tea.NewProgram(initial, tea.WithInputTTY(), tea.WithOutput(out), tea.WithAltScreen())
|
||||||
reporter := &TUIReporter{program: p, debug: debug, closed: make(chan struct{})}
|
reporter := &TUIReporter{program: p, debug: debug, closed: make(chan struct{})}
|
||||||
@@ -110,7 +115,19 @@ func NewTUIReporter(debug bool, out io.Writer, buildInfo BuildInfo, cancel conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rendered, ok := model.(*modelState); ok {
|
if rendered, ok := model.(*modelState); ok {
|
||||||
_, _ = fmt.Fprintln(out, rendered.View())
|
output := rendered.FinalView()
|
||||||
|
if len(rendered.summaryStreams) == 0 {
|
||||||
|
fallback := reporter.snapshotSummary()
|
||||||
|
if len(fallback) > 0 {
|
||||||
|
tmp := &modelState{
|
||||||
|
summaryStreams: fallback,
|
||||||
|
width: rendered.width,
|
||||||
|
status: summaryStatusAllDone(),
|
||||||
|
}
|
||||||
|
output = tmp.FinalView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(out, output)
|
||||||
}
|
}
|
||||||
close(reporter.closed)
|
close(reporter.closed)
|
||||||
}()
|
}()
|
||||||
@@ -165,12 +182,28 @@ func (r *TUIReporter) Error(step cameradar.Step, err error) {
|
|||||||
|
|
||||||
// Summary implements Reporter.
|
// Summary implements Reporter.
|
||||||
func (r *TUIReporter) Summary(streams []cameradar.Stream, _ error) {
|
func (r *TUIReporter) Summary(streams []cameradar.Stream, _ error) {
|
||||||
r.send(summaryMsg{streams: copyStreams(streams), final: true})
|
cloned := copyStreams(streams)
|
||||||
|
r.recordSummary(cloned)
|
||||||
|
r.send(summaryMsg{streams: cloned, final: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSummary updates the summary section with partial results.
|
// UpdateSummary updates the summary section with partial results.
|
||||||
func (r *TUIReporter) UpdateSummary(streams []cameradar.Stream) {
|
func (r *TUIReporter) UpdateSummary(streams []cameradar.Stream) {
|
||||||
r.send(summaryMsg{streams: copyStreams(streams), final: false})
|
cloned := copyStreams(streams)
|
||||||
|
r.recordSummary(cloned)
|
||||||
|
r.send(summaryMsg{streams: cloned, final: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TUIReporter) recordSummary(streams []cameradar.Stream) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.last = streams
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TUIReporter) snapshotSummary() []cameradar.Stream {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return copyStreams(r.last)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements Reporter.
|
// Close implements Reporter.
|
||||||
@@ -346,33 +379,153 @@ func progressWidth(width int) int {
|
|||||||
return 36
|
return 36
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSummaryTables(streams []cameradar.Stream, width int, status map[cameradar.Step]state, final bool) []summaryTable {
|
func buildSummaryTables(streams []cameradar.Stream, width int, status map[cameradar.Step]state, maxRows int) []summaryTable {
|
||||||
visibility := summaryVisibility(status)
|
visibility := summaryVisibility(status)
|
||||||
accessible, others := partitionStreams(streams)
|
accessible, others := partitionStreams(streams)
|
||||||
rows := append(buildSummaryRows(accessible, visibility), buildSummaryRows(others, visibility)...)
|
rows := append(buildSummaryRows(accessible, visibility), buildSummaryRows(others, visibility)...)
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
message := "Waiting for results..."
|
rows = []table.Row{emptySummaryRow()}
|
||||||
if final {
|
}
|
||||||
message = "No streams discovered."
|
|
||||||
}
|
if maxRows > 0 {
|
||||||
return []summaryTable{{title: "Streams", emptyMessage: message}}
|
switch {
|
||||||
|
case len(rows) > maxRows:
|
||||||
|
if maxRows == 1 {
|
||||||
|
rows = []table.Row{summaryOverflowRow(len(rows))}
|
||||||
|
} else {
|
||||||
|
visibleRows := maxRows - 1
|
||||||
|
hidden := len(rows) - visibleRows
|
||||||
|
rows = append(rows[:visibleRows], summaryOverflowRow(hidden))
|
||||||
|
}
|
||||||
|
case len(rows) < maxRows:
|
||||||
|
rows = padSummaryRows(rows, maxRows)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
title := fmt.Sprintf("Streams (%d accessible / %d total)", len(accessible), len(streams))
|
|
||||||
columns := summaryColumns(width, rows)
|
columns := summaryColumns(width, rows)
|
||||||
model := table.New(
|
model := table.New(
|
||||||
table.WithColumns(columns),
|
table.WithColumns(columns),
|
||||||
table.WithRows(rows),
|
table.WithRows(rows),
|
||||||
table.WithFocused(false),
|
table.WithFocused(false),
|
||||||
table.WithHeight(len(rows)+1),
|
table.WithHeight(len(rows)),
|
||||||
)
|
)
|
||||||
model.SetStyles(summaryTableStyles())
|
model.SetStyles(summaryTableStyles())
|
||||||
|
|
||||||
return []summaryTable{{title: title, table: model}}
|
return []summaryTable{{table: model}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSummaryTitle(streams []cameradar.Stream) string {
|
||||||
|
accessible, _ := partitionStreams(streams)
|
||||||
|
return fmt.Sprintf("Summary - Streams (%d accessible / %d total)", len(accessible), len(streams))
|
||||||
|
}
|
||||||
|
|
||||||
|
func summaryStatusAllDone() map[cameradar.Step]state {
|
||||||
|
status := make(map[cameradar.Step]state)
|
||||||
|
for _, step := range cameradar.Steps() {
|
||||||
|
status[step] = stateDone
|
||||||
|
}
|
||||||
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyEntry = "—"
|
const emptyEntry = "—"
|
||||||
|
|
||||||
|
func emptySummaryRow() table.Row {
|
||||||
|
row := make(table.Row, summaryColumnCount)
|
||||||
|
for i := range row {
|
||||||
|
row[i] = emptyEntry
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func padSummaryRows(rows []table.Row, maxRows int) []table.Row {
|
||||||
|
for len(rows) < maxRows {
|
||||||
|
rows = append(rows, emptySummaryRow())
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func summaryOverflowRow(hidden int) table.Row {
|
||||||
|
row := emptySummaryRow()
|
||||||
|
if hidden <= 0 {
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
label := "\u2026 1 more stream"
|
||||||
|
if hidden > 1 {
|
||||||
|
label = fmt.Sprintf("\u2026 %d more streams", hidden)
|
||||||
|
}
|
||||||
|
row[0] = label
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSummaryTablePlain(columns []table.Column, rows []table.Row) string {
|
||||||
|
colWidths := make([]int, len(columns))
|
||||||
|
for i, col := range columns {
|
||||||
|
colWidths[i] = max(col.Width, len([]rune(col.Title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(renderSummaryBorder("┌", "┬", "┐", colWidths))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(renderSummaryRow(columnTitles(columns), colWidths))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(renderSummaryBorder("├", "┼", "┤", colWidths))
|
||||||
|
for _, row := range rows {
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(renderSummaryRow(row, colWidths))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
builder.WriteString(renderSummaryBorder("└", "┴", "┘", colWidths))
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSummaryBorder(left, middle, right string, widths []int) string {
|
||||||
|
parts := make([]string, 0, len(widths))
|
||||||
|
for _, width := range widths {
|
||||||
|
parts = append(parts, strings.Repeat("─", width+2))
|
||||||
|
}
|
||||||
|
return left + strings.Join(parts, middle) + right
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSummaryRow(cells []string, widths []int) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("│")
|
||||||
|
for i, width := range widths {
|
||||||
|
value := ""
|
||||||
|
if i < len(cells) {
|
||||||
|
value = cells[i]
|
||||||
|
}
|
||||||
|
builder.WriteString(" ")
|
||||||
|
builder.WriteString(padAndTrim(value, width))
|
||||||
|
builder.WriteString(" │")
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func padAndTrim(value string, width int) string {
|
||||||
|
if width <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
runes := []rune(value)
|
||||||
|
if len(runes) > width {
|
||||||
|
return string(runes[:width])
|
||||||
|
}
|
||||||
|
if len(runes) < width {
|
||||||
|
return string(runes) + strings.Repeat(" ", width-len(runes))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnTitles(columns []table.Column) []string {
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
titles := make([]string, len(columns))
|
||||||
|
for i, col := range columns {
|
||||||
|
titles[i] = col.Title
|
||||||
|
}
|
||||||
|
return titles
|
||||||
|
}
|
||||||
|
|
||||||
func buildSummaryRows(streams []cameradar.Stream, visibility summaryVisibilityState) []table.Row {
|
func buildSummaryRows(streams []cameradar.Stream, visibility summaryVisibilityState) []table.Row {
|
||||||
rows := make([]table.Row, 0, len(streams))
|
rows := make([]table.Row, 0, len(streams))
|
||||||
for _, stream := range streams {
|
for _, stream := range streams {
|
||||||
|
|||||||
Reference in New Issue
Block a user