render

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: AGPL-3.0 Imports: 25 Imported by: 0

Documentation

Overview

Package render turns theme roles into concrete lipgloss styles and renders structured event bodies (diffs, markdown, dialogs, headers) into styled text. Every function here is pure: input in, string out, no I/O and no package state.

Markdown renders CommonMark + GFM (headings, paragraphs, lists, ordered + task lists, blockquotes, fenced + indented + inline code, tables, links, horizontal rules, strikethrough, emphasis/strong) through charm.land/glamour/v2, mapped to the theme palette so a markdown block matches the rest of the screen at every colour tier.

Package render turns theme roles into concrete lipgloss styles and renders structured event bodies (diffs, minimal markdown) into styled text. Every function here is pure: input in, string out, no I/O, no package state.

Index

Constants

View Source
const MinSplitDiffWidth = 60

MinSplitDiffWidth is the minimum terminal width required to render a side-by-side split diff. Below this width, FormatDiffLines falls back to a clean unified diff to preserve legibility.

View Source
const SplitNavMax = 60

SplitNavMax caps the navigation pane's absolute width: a share of a very wide terminal is still too much list (30% of 300 columns is 90 columns of paths). Past the cap the reading pane takes everything extra.

View Source
const SplitNavShare = 30

SplitNavShare is the width share the navigation pane (a list the user picks from) takes in a two-pane split, in percent. 30 keeps a path list readable and leaves the reading pane the reading measure (gh-dash's list/detail ratio is the same shape).

Variables

This section is empty.

Functions

func Bordered

func Bordered(t theme.Theme, tier theme.Tier, r theme.Role, width int, content string) string

Bordered wraps content in a rounded border whose colour is the given border role resolved at the tier. Like WithBg, a tier with no colour for the role (the no-colour/ASCII tiers) still draws the border, just uncoloured: the box is structure, and structure survives NO_COLOR.

width is the INNER width in cells; width <= 0 sizes the box to the content (lipgloss default). A caller whose content windows or scrolls must pass a fixed width, or the border breathes with every content change - the box must not move while the user reads it.

func BorderedWithHint

func BorderedWithHint(t theme.Theme, tier theme.Tier, r, hintRole theme.Role, width int, content, hint string) string

BorderedWithHint wraps content in a rounded border with an optional right-aligned hint in the top border row, drawn in hintRole. The role is a parameter because the hint is not always the same kind of thing: the composer's is a key reminder (subtle), the approval prompt's names a state the reader must not miss (warning).

func Columns

func Columns(gap int, rows [][]string) []string

Columns aligns a table of pre-styled cells into columns: every column is padded to the widest cell that occupies it across the whole row set, so status/count fields land in the same screen column on every row instead of drifting with each row's own content width (the settings sections' failure mode this replaces).

gap is the number of plain spaces between one column's padded content and the next. Rows may carry a different number of cells (a "ragged" table): a column only exists where some row supplies it, and a shorter row is never padded past its own last cell, so it produces no trailing whitespace.

Cells may already carry ANSI styling; width is measured with ansi.StringWidth so padding accounts for display width, not byte length. Pure: input in, string slice out, no I/O and no package state.

func ContextRole

func ContextRole(pct int) theme.Role

ContextRole returns the semantic theme role for a context-fill percentage. The thresholds match the topbar's existing coloring contract (70/90) with an additional RoleInfo step below 70% to surface early signal in the status line, where the ctx pill currently renders with no color at any fill level.

pct >= 90 → RoleDanger (critical, matches existing topbar danger boundary) pct >= 70 → RoleWarning (caution, matches existing topbar warning boundary) pct >= 50 → RoleInfo (informational notice: context is in active use) pct < 50 → RoleFGSubtle (neutral; comparison-based so pct > 100 is still danger)

func Dialog

func Dialog(t theme.Theme, tier theme.Tier, width, height int, title, body, hint string) string

Dialog renders title, body, and hint inside a bordered, inset-filled box centered on a width x height terminal. It is the one centered dialog primitive: this renderer has no compositing layer, so the dialog is the whole frame and the padding around the box carries the base background - the conversation behind it is not shown.

Content that does not fit is truncated, not scrolled: lines clip to the inner width and surplus body rows drop off the bottom. A scrolling body is deliberately out of scope for v1; every caller today (theme preview, model picker, help) fits or is better served by the pager for full-height reading.

Degrade tiers follow the same ladder as WithBg and Bordered: at ASCII/NoTTY the inset and base backgrounds contribute nothing (a colour fill without colour is broken), and the border stays as plain glyphs - structure survives NO_COLOR.

func DialogBodyRows

func DialogBodyRows(height int) int

DialogBodyRows is how many body rows a Dialog framed to height can show, after its margins, border, padding, title, separators, and hint. A caller that windows scrollable body content by rows must use this number: scrolling by a larger step leaves tail rows unreachable, because Dialog clips what does not fit.

func DialogBodyWidth

func DialogBodyWidth(width int) int

DialogBodyWidth is the inner width a Dialog framed to width gives its body rows, after the same margin shrinking the clip applies. A caller that renders its own surface INTO the body (an embedded chat, not a plain string) sizes it to this so the clip never has to cut it.

func DialogHitsBackdrop

func DialogHitsBackdrop(width, height, contentRowCount, clickX, clickY int) bool

DialogHitsBackdrop reports whether a click at (clickX, clickY) landed outside the centered dialog box.

func DialogHitsClose

func DialogHitsClose(width, height, contentRowCount, clickX, clickY int) bool

DialogHitsClose reports whether a click at (clickX, clickY) lands on the [x] close button area of a dialog centered on a width x height terminal.

func DialogNoBackdrop

func DialogNoBackdrop(t theme.Theme, tier theme.Tier, width, height int, title, body, hint string) string

DialogNoBackdrop renders title, body, and hint inside a bordered, inset-filled dialog box that fills the given width x height exactly without any outer margin/backdrop.

func DialogNoBackdropBodyRows

func DialogNoBackdropBodyRows(height int) int

DialogNoBackdropBodyRows is how many body rows a DialogNoBackdrop framed to height can show (height less 2 border rows, 2 padding rows, and 3 title/separator/hint rows).

func DialogNoBackdropBodyWidth

func DialogNoBackdropBodyWidth(width int) int

DialogNoBackdropBodyWidth is the inner width a DialogNoBackdrop framed to width gives its body rows (width less 2 border columns and 4 padding columns).

func DialogNoBackdropHitsClose

func DialogNoBackdropHitsClose(width, height, clickX, clickY int) bool

DialogNoBackdropHitsClose reports whether a click at (clickX, clickY) lands on the [x] close button of a DialogNoBackdrop box sized to width x height.

func Diff

func Diff(t theme.Theme, tier theme.Tier, d uievent.Diff) string

Diff renders the hunks of a unified diff as styled text. Hunk headers use RoleDiffHunk; added/removed lines use the diff fg/bg role pairs; context lines use RoleFGMuted with no background.

It renders NO summary line. The path and the +N -M counts belong to the enclosing block's header, in its detail and meta columns (wireframes-panes.md section 11), and rendering them here too printed them twice.

func DiffLineCount

func DiffLineCount(d uievent.Diff) int

DiffLineCount is how many lines Diff renders: one per hunk header plus one per diff line.

func DiffLines

func DiffLines(t theme.Theme, tier theme.Tier, d uievent.Diff) []string

DiffLines is Diff as one string per rendered line, so surfaces that window into a diff (the scrollable approval preview) can slice it without re-parsing styled text.

func FillBG

func FillBG(t theme.Theme, tier theme.Tier, r theme.Role, text string) string

FillBG paints text onto role r's background.

A lipgloss Background() on a block colours only up to the first SGR reset inside it, and styled text is a chain of runs that each end in one. Every run after the first therefore drew on the terminal's own background: dark rectangles behind a light dialog's code sample, and a screen that kept the terminal's colour after the theme changed. FillBG re-establishes the background after every reset, so a fill is whole rather than patchy.

A run that carries its OWN background (a diff line, a selected picker row) still wins: this only paints what is otherwise unpainted, and picks up again where that run resets.

Callers pad rows to the width they want covered first - FillBG colours the cells it is given and adds none.

It returns text unchanged at a tier with no colour for r, matching the degradation ladder WithBg and Bordered follow: a colour fill without colour is nothing, and NO_COLOR output must stay byte-identical.

func FormatArgs

func FormatArgs(args map[string]any) string

FormatArgs renders a tool-call argument map as a stable, sorted "k=v k2=v2" string. Shared by every component that displays a tool call (transcript, approval).

func FormatCommandOutput

func FormatCommandOutput(t theme.Theme, tier theme.Tier, output string, ok bool, width int) ([]string, bool)

func FormatDiagnosticsOutput

func FormatDiagnosticsOutput(t theme.Theme, tier theme.Tier, output string, width int) []string

FormatDiagnosticsOutput formats compiler / linter diagnostic lines with severity markers.

func FormatDiffLines

func FormatDiffLines(t theme.Theme, tier theme.Tier, width int, d uievent.Diff) []string

FormatDiffLines renders diffs using side-by-side columns when width >= MinSplitDiffWidth, and falls back to unified DiffLines when narrower.

func FormatDispatchTasksOutput

func FormatDispatchTasksOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatDispatchTasksOutput formats a dispatch_tasks result into one line per task instead of the raw JSON a generic dump would otherwise produce. dispatch_tasks returns one of two shapes depending on "wait": the default wait="run" blocks for the batch and returns a bare JSON array of per-task envelopes (handled here); wait="none"/"task" returns immediately with the same {"run_id":...,"task_results":[...]} envelope inspect_agents/join_run use, delegated to FormatOrchestrationRunOutput rather than duplicated.

func FormatFileReadOutput

func FormatFileReadOutput(t theme.Theme, tier theme.Tier, output string, width int) ([]string, bool)

FormatFileReadOutput previews file content with line numbers when long.

func FormatFileReadOutputWithContext

func FormatFileReadOutputWithContext(t theme.Theme, tier theme.Tier, filePath string, startLine int, output string, width int) ([]string, bool)

FormatFileReadOutputWithContext previews file content with syntax highlighting and line numbers.

func FormatGrepOutput

func FormatGrepOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatGrepOutput groups grep/search results by file.

func FormatGrepOutputWithContext

func FormatGrepOutputWithContext(t theme.Theme, tier theme.Tier, query, output string, width int) (string, []string)

FormatGrepOutputWithContext groups grep/search results by file supporting both NDJSON and standard ripgrep output.

func FormatInspectRepositoryOutput

func FormatInspectRepositoryOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatInspectRepositoryOutput formats an inspect_repository result - matches grouped by file with line numbers, mirroring the grouping FormatGrepOutputWithContext already uses for grep/glob/symbol tools - instead of a raw JSON dump of the provenance+results envelope.

func FormatJSONOutput

func FormatJSONOutput(t theme.Theme, tier theme.Tier, output string, width int) []string

FormatJSONOutput formats JSON objects or arrays into readable key-value summary lines or syntax-highlighted json.

func FormatLedgerOutput

func FormatLedgerOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatLedgerOutput formats ledger/output responses into clean content blocks without envelope metadata, differentiating not-found refs, recorded errors/messages, and malformed-ref shapes instead of treating every response identically.

func FormatListDirOutput

func FormatListDirOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatListDirOutput formats directory listings into clean tree rows.

func FormatMemoryOutput

func FormatMemoryOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatMemoryOutput renders memory search/list results into clean memory cards.

func FormatMessagingOutput

func FormatMessagingOutput(t theme.Theme, tier theme.Tier, name string, args map[string]any, output string, width int) (summary string, body []string, collapsible bool)

FormatMessagingOutput formats inter-agent messages and blackboard entries into structured cards.

func FormatOrchestrationRunOutput

func FormatOrchestrationRunOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatOrchestrationRunOutput formats an inspect_agents/spawn_agent/ join_run result into a run summary, a per-task status list, and any pending parked questions, instead of a raw JSON dump.

func FormatRunEventsOutput

func FormatRunEventsOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatRunEventsOutput formats a list_run_events result into a chronological, icon-coded event timeline instead of a raw JSON dump.

func FormatToolDetail

func FormatToolDetail(name string, args map[string]any) string

FormatToolDetail renders a domain-specific, readable detail string for a tool call. It formats commands with "$ <cmd>", file tools with their relative path, search tools with their query/scope, and web tools with their domain, falling back to FormatArgs for generic tools.

func FormatToolOutput

func FormatToolOutput(t theme.Theme, tier theme.Tier, name, output string, ok bool, width int) (detail string, body []string, collapsible bool)

FormatToolOutput formats raw tool outputs (commands, grep searches, file reads, JSON payloads) into clean, structured, readable transcript lines styled through theme roles.

func FormatToolOutputWithContext

func FormatToolOutputWithContext(t theme.Theme, tier theme.Tier, name string, args map[string]any, output string, ok bool, width int) (detail string, body []string, collapsible bool)

FormatToolOutputWithContext formats raw tool outputs using both tool name and parsed tool arguments.

func FormatWorkflowOutput

func FormatWorkflowOutput(t theme.Theme, tier theme.Tier, output string, width int) (string, []string)

FormatWorkflowOutput formats workflow execution results into summary and step items.

func HardWrap

func HardWrap(line string, width int) []string

HardWrap breaks one logical line into the terminal rows it occupies at width, cutting mid-token. Tool output and code are not prose: a break on a word boundary would misrepresent the bytes the tool produced.

It is ANSI-aware, so a styled line keeps its escapes and they cost no display columns.

func Header(t theme.Theme, tier theme.Tier, width int, spec HeaderSpec) string

Header renders a block header, placing meta and state inline right after the detail. When the columns cannot all fit, the DETAIL is clipped and marked - never the state, which carries meaning, and never the label, which identifies the block.

Contract, relied on by Block.Height: for width > 0 the result is AT MOST width display columns, on one row, and contains no newline. A header that overflowed would wrap, and the live window would then budget one row for content that draws two.

Pure: input in, string out, no I/O and no package state.

func Help

func Help(t theme.Theme, tier theme.Tier, rows []keymap.HelpRow) string

Help renders the keymap as a styled block, grouped by context.

It is GENERATED from the binding table, never hand-written. A help screen maintained beside the dispatch table drifts from it, and the drift is invisible until a user presses a key the help promised.

Pure: input in, string out.

func HighlightCode

func HighlightCode(t theme.Theme, tier theme.Tier, filenameOrLang, code string) []string

HighlightCode applies syntax highlighting to source code based on filename or language identifier. It returns a slice of styled lines. At TierASCII or TierNoTTY, ANSI escapes are omitted.

func HintFits

func HintFits(width int, hint string) bool

HintFits reports whether BorderedWithHint can place hint in the top border row of a box of the given width.

It is exported because the hint is not always decoration: the approval prompt puts WHAT is being approved there, and a caller that would lose content when the hint is dropped has to ask before deciding where to put it. The rule lives here, beside the renderer that applies it, so the two cannot disagree.

func Lip

func Lip(s theme.Style) lipgloss.Style

Lip converts a resolved theme.Style into a foreground lipgloss.Style. Structural emphasis (Bold/Dim) applies regardless of colour tier so it survives NO_COLOR; colour is applied only when the Style carries one.

func Markdown

func Markdown(t theme.Theme, tier theme.Tier, width int, in string) (out string)

Markdown renders assistant markdown as ANSI-styled text using the theme palette. It is tier-aware: every role's hex (truecolor/256), ANSI16 index (16-colour), or absence (ASCII/NoTTY) flows through Glamour's style config and chroma formatter.

It returns in unchanged when Glamour returns an error or panics. Glamour surfaces markdown errors as strings rather than panics, and the renderer wraps its body in defer recover() because goldmark plugins have historically been panic-prone in adjacent code paths. The cost is one deferred closure per call; the alternative - a crashed transcript - is not acceptable.

A width of 0 or negative is clamped to minMarkdownWidth so Glamour never sees a 0-column wrap. The transcript already guards this with max(20, width-2); this floor is the last line of defence.

func ProgressBar

func ProgressBar(width, step, total int) string

ProgressBar draws the subagent progress bar from the section 3 glyph table: "#" for done, "." for remaining, with the percentage beside it. Both glyphs are ASCII, so the bar is identical at every colour tier.

func ProseMeasure

func ProseMeasure(width int) int

ProseMeasure is the wrap width for prose at a given terminal width. Prose wraps to a measure, not to the terminal, because a full-width line on a wide terminal is hard to read (wireframes-panes.md 14).

func Role

func Role(t theme.Theme, tier theme.Tier, r theme.Role) lipgloss.Style

Role resolves a theme role at the given tier and returns its foreground lipgloss style, the common case for component call sites.

func Split

func Split(t theme.Theme, tier theme.Tier, width, height int, focus Side, left, right string) string

Split composes the reading column and the nav sidebar side by side, separated by a 1-column gutter. The nav sidebar has a subtle card background (RoleBGSubtle) matching the composer where the user types.

Both columns draw exactly height rows: content is padded, and surplus rows clip from the bottom (Split never scrolls - windowing belongs to the pane's owner, so there stays one implementation of it).

This is the codebase's only side-by-side composition; it exists so the ratio, the height contract, and the layout convention live in one place when more panes arrive.

func SplitAt

func SplitAt(t theme.Theme, tier theme.Tier, width, height, leftWidth int, focus Side, left, right string) string

SplitAt is Split with the left pane's content width given directly instead of derived from SplitWidths. Split's 30%-capped-at-60 share is one policy for one caller (a file list beside a diff); a settings screen's five-word nav sidebar needs a different share and a different cap, and neither belongs in this package (uikitconfig owns literals - see internal/uikit/config's package doc). SplitAt is the geometry both policies share: left is drawn at exactly leftWidth, separated by a 1-column gutter, and right takes what remains with RoleBGSubtle background.

func SplitAtDialog

func SplitAtDialog(t theme.Theme, tier theme.Tier, width, height, leftWidth int, navFocused bool, title, body, hint, nav string) string

SplitAtDialog is SplitDialog with the dialog's (left pane's) content width given directly, the SplitDialog counterpart to SplitAt: a caller whose nav sidebar is not Split's 30%-capped-at-60 share still wants its editor to open as a dialog beside that sidebar rather than full-width.

func SplitDialog

func SplitDialog(t theme.Theme, tier theme.Tier, width, height int, navFocused bool, title, body, hint, nav string) string

SplitDialog is the split with its reading column replaced by a centered dialog: the dialog is sized to the reading column's whole area, so the blocks compose with no gap, and the nav pane stays visible and legible beside it instead of hiding behind a full-surface dialog.

func SplitDiff

func SplitDiff(t theme.Theme, tier theme.Tier, width int, d uievent.Diff) string

SplitDiff renders the hunks of a diff in side-by-side (split) column format.

func SplitDiffLines

func SplitDiffLines(t theme.Theme, tier theme.Tier, width int, d uievent.Diff) []string

SplitDiffLines renders each hunk as aligned side-by-side rows with line numbers.

func SplitListDetail

func SplitListDetail(listLines, detailLines []string, selectedLine, height int, notice string) string

SplitListDetail lays out a scrollable row list above an optional collapsible detail preview, sharing one height budget, with a pinned notice line at the bottom when present. It is the shared geometry the settings screen's grouped sections need: a header/row list on top, window-scrolled to keep the selection visible, and a preview of the selected row beneath it, extracted from the Skills section so the Agents section (and any future grouped section) can present the same layout without re-deriving the same height math.

listLines is every list row already rendered (including any group headers) in display order. detailLines is the selected item's pre-rendered preview body, or nil when nothing is selected or the section has no preview to show for the current row. selectedLine is the listLines index the window must keep visible; pass a negative value when the list is empty. height is the section's full available row budget. notice, when non-empty, must already be styled by the caller (this package has no theme access here) and consumes its own trailing row, taking priority over detail rows the same way the section's own notice line always has.

func SplitWidths

func SplitWidths(width int) (reading, nav int)

SplitWidths is the pane geometry Split assigns: nav is the right pane's width including its divider column, at SplitNavShare of width capped at SplitNavMax; reading is the left pane's. Exported so a caller that renders content INTO a pane sizes that content to the same numbers the frame draws around it - blocks pad and clip, they never re-wrap, so wide rows are the caller's to cut.

func WindowSlice

func WindowSlice(total, cursor, maxRows int) (start, end int)

WindowSlice calculates the [start, end) index range to window total items within maxRows visible lines such that cursor is guaranteed to be within [start, end). If maxRows <= 0 or total <= maxRows, it returns (0, total).

func WithBg

func WithBg(st lipgloss.Style, t theme.Theme, tier theme.Tier, r theme.Role) lipgloss.Style

WithBg layers a background role onto an existing style. A tier with no colour for the role (e.g. the no-colour/ASCII tier) contributes no background, matching the degradation ladder.

func Wrap

func Wrap(text string, measure int) []string

Wrap breaks text at the given measure on word boundaries, preserving existing newlines. A word longer than the measure is left whole rather than split: breaking an identifier or a URL mid-token hurts more than one long line does.

Pure: input in, string slice out. Every width here is a DISPLAY width, not a byte count. A byte count wraps accented prose at half the measure and CJK at a third of it.

Types

type HeaderSpec

type HeaderSpec struct {
	// Marker is the collapse glyph, "v", ">" or " ".
	Marker  string
	Label   string
	Detail  string
	DiffAdd int
	DiffDel int
	Meta    string
	State   string
	// StateRole colours the state word. The word is always rendered, so
	// meaning never depends on colour alone.
	StateRole theme.Role
}

HeaderSpec is the content of a block header row, in four columns. wireframes-panes.md section 2: the collapse marker at column 1, the label at column 3, the detail after it, then meta and state placed inline immediately after the detail, separated by minHeaderGap.

func SanitizeSpec

func SanitizeSpec(spec HeaderSpec) HeaderSpec

SanitizeSpec cleans every string field in spec of control and format characters.

type Side

type Side int

Side names the pane of a split that holds keyboard focus.

const (
	Left Side = iota
	Right
)

Left and Right are the two panes of a split.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL