package tui import ( "strings" "github.com/charmbracelet/lipgloss" ) // section is a styled command-output block: a horizontal rule carrying // the title, an optional subtitle on the rule, a body of pre-formatted // lines (callers preserve their own indent), and an optional footer // separated from the body by a blank line. The renderer is a thin // presentation layer over the existing styles palette; it adds no new // colour and introduces no new write path. type section struct { title string subtitle string body []string footer []string } // sectionRow is one entry in a key/value table that flows into the body. // Callers build rows with rowsToBody before constructing a section so // the section struct stays narrow. type sectionRow struct { key, value, note string } // defaultRuleWidth is the fill cap used when the model has not yet seen // a WindowSizeMsg (width 0). Terminals usually wrap longer rules cleanly, // but a hard cap keeps the visual frame bounded on first paint. const defaultRuleWidth = 60 // renderSection formats s as ordered scrollback lines. A leading blank // line gives the section breathing room after the echoed prompt; the // header line is `─── title · subtitle ──…` with the trailing rule // filling to width. Each body line is emitted verbatim; the footer // (if any) is prefixed by a single blank line. Pure: callers print the // result with tea.Println via the existing printLines helper. func renderSection(width int, st styles, s section) []string { out := make([]string, 0, len(s.body)+len(s.footer)+4) out = append(out, "") out = append(out, renderRule(width, st, s.title, s.subtitle)) out = append(out, s.body...) if len(s.footer) > 0 { out = append(out, "") out = append(out, s.footer...) } return out } // renderRule builds the section header. The shape is // // ─── title · subtitle ────────────────── // // with the title in the `key` style and the subtitle (when set) in // `dim`. The leading three dashes and the trailing fill use `dimmer` // so the title is the visual anchor. Width 0 falls back to // defaultRuleWidth. func renderRule(width int, st styles, title, subtitle string) string { w := width if w <= 0 { w = defaultRuleWidth } const lead = "─── " plain := lead + title if subtitle != "" { plain += " · " + subtitle } fillN := w - lipgloss.Width(plain) - 1 if fillN < 3 { fillN = 3 } styled := st.dimmer.Render(lead) + st.key.Render(title) if subtitle != "" { styled += " " + st.dim.Render("· "+subtitle) } styled += st.dimmer.Render(" " + strings.Repeat("─", fillN)) return styled } // tableBody flows a key/value/note table into body lines with an // optional header row + rule separator. The key column is rendered in // `key` style; the value column in `value` style (readable primary // foreground); the note column in `dim`. The header cells use // `tableHeader`; the separator rule uses `dimmer`. Columns pad to the // widest cell across the header and every row so notes stay vertically // aligned. Leading two-space indent matches the rest of the body // convention. An all-empty head omits the header rows entirely. func tableBody(st styles, head [3]string, rows []sectionRow) []string { if len(rows) == 0 { return nil } maxKey := lipgloss.Width(head[0]) maxVal := lipgloss.Width(head[1]) maxNote := lipgloss.Width(head[2]) for _, r := range rows { if n := lipgloss.Width(r.key); n > maxKey { maxKey = n } if n := lipgloss.Width(r.value); n > maxVal { maxVal = n } if n := lipgloss.Width(r.note); n > maxNote { maxNote = n } } hasHead := head[0] != "" || head[1] != "" || head[2] != "" out := make([]string, 0, len(rows)+2) if hasHead { kPad := strings.Repeat(" ", maxKey-lipgloss.Width(head[0])) vPad := strings.Repeat(" ", maxVal-lipgloss.Width(head[1])) line := " " + st.tableHeader.Render(head[0]) + kPad + " " + st.tableHeader.Render(head[1]) + vPad if head[2] != "" { line += " " + st.tableHeader.Render(head[2]) } out = append(out, line) sep := " " + st.dimmer.Render(strings.Repeat("─", maxKey)) + " " + st.dimmer.Render(strings.Repeat("─", maxVal)) if maxNote > 0 { sep += " " + st.dimmer.Render(strings.Repeat("─", maxNote)) } out = append(out, sep) } for _, r := range rows { kPad := strings.Repeat(" ", maxKey-lipgloss.Width(r.key)) vPad := strings.Repeat(" ", maxVal-lipgloss.Width(r.value)) line := " " + st.key.Render(r.key) + kPad + " " + st.value.Render(r.value) + vPad if r.note != "" { line += " " + st.dim.Render(r.note) } out = append(out, line) } return out } // renderError produces a single styled line for short failure or // validation messages: `