Documentation
¶
Overview ¶
Package ui holds what every hoist screen shares: one Styles palette built from a light/dark flag, the frame and pane chrome every screen is drawn in (frame.go), the dialog compositor (dialog.go), relative-time wording (time.go) and the status-bar line helper. It imports Lip Gloss and x/ansi (cell width and ANSI stripping) — no Bubbles components, no screen state — so any screen package can depend on it without a cycle.
Index ¶
- Constants
- func Ago(now, t time.Time) string
- func BodyHeight(height, sections int) int
- func BoolInt(b bool) int
- func Box(st Styles, title string, sections []string, width int) string
- func Columns(st Styles, left, right string, leftWidth int) string
- func Dialog(st Styles, under, title, body string, width, height int) string
- func DialogWidth(w int) int
- func NoticeLines(st Styles, text string, width int) []string
- func NoticeLinesStyled(style lipgloss.Style, text string, width int) []string
- func Plural(n int, one string) string
- func ShortSHA(sha string) string
- func Span(d time.Duration) string
- func StatusBar(width int, left, right string) string
- func StepGlyph(s StepState) string
- func Until(now, t time.Time) string
- type Frame
- type StepState
- type Styles
Constants ¶
const NoticeMaxLines = 3
NoticeMaxLines caps how many terminal rows a root-level notice may take. A notice is reserved space stolen from the screen under it (app.Model.View), so an unbounded one — a git or forge transport error can run to several hundred characters — would push the screen it is explaining off the terminal instead.
Variables ¶
This section is empty.
Functions ¶
func BodyHeight ¶
BodyHeight is how many content rows a Frame with n sections can hold on a terminal height rows tall: the height minus the footer, the two edges and the n-1 rules between sections. A screen sizes its scrolling section to this less the fixed rows of its other sections.
func BoolInt ¶ added in v0.3.0
BoolInt is 1 for true and 0 for false, for row arithmetic like `rows -= h * ui.BoolInt(cond)`.
func Box ¶
Box draws a titled, rounded box exactly width cells wide around sections, with a rule between each pair. Every line of every section is truncated to the inner width, never wrapped: a version string that wraps mid-token is the defect this exists to end (#85).
func Columns ¶
Columns lays left and right side by side with a vertical rule between them, left padded or truncated to leftWidth, both aligned to the top. Replaces the string-padding joinPanes the plan screen carried (AGENTS.md §4.8 codifies this as the two-pane shape).
func Dialog ¶
Dialog draws body in a titled box centred over under, for a width×height terminal — the shape every huh.Confirm takes from M10 on. The screen underneath stays visible (dimmed, its own colours stripped) so the decision keeps its context: the operator confirming a direct commit is still looking at the commits it ships. Built on lipgloss's compositor (Canvas and Layer), which is the one place the TUI uses it: a dialog is the case where covering the parent is the point, and everything else composes with JoinVertical (docs/tui/README.md).
The dialog is at least wide enough for its longest line plus the box, capped at the terminal; body lines wider than that are truncated. A terminal too small for the box gets the dialog alone, uncentred, so the question is never lost behind the chrome.
func DialogWidth ¶ added in v0.3.0
DialogWidth is the width of a modal dialog's body on a terminal w columns wide: 8 columns narrower than the terminal (the dimmed parent shows around it), never wider than 72 and never narrower than 20. Every screen that raises a huh field in a ui.Dialog sizes it with this, so the dialogs agree on a width rather than each screen repeating the clamp.
func NoticeLines ¶ added in v0.2.0
NoticeLines renders a transient message as at most NoticeMaxLines rows of exactly width cells, word-wrapped, with the overflow marked "…" on the last row. It returns nil for an empty message so the caller can size the screen above it by len(lines).
The caller is expected to shrink whatever it draws above by exactly this many rows: a Frame renders exactly `height` lines (Render, above), so a notice appended after one lands on row height+1 and the alternate screen buffer never shows it — the whole defect this exists to close (#164).
func NoticeLinesStyled ¶ added in v0.2.0
NoticeLinesStyled is NoticeLines with the rendering style named explicitly, rather than always st.Notice: the root's activity row colours itself by the entry's own kind — Info for a plain report, Good for a landed/completed outcome, Bad for a refusal or failure — instead of every entry reading in the same amber regardless of what happened.
func Plural ¶ added in v0.2.0
Plural renders a count with a word that only takes a bare "s" — "1 Deployment", "2 Deployments" — replacing every screen's own "Deployment(s)" (AGENTS.md §9, the audit doc's UX-L2) with one place that gets English right for both n=1 and n=0 ("0 Deployments", never "0 Deployment(s)").
func ShortSHA ¶ added in v0.3.0
ShortSHA shortens a commit sha for display to its first 12 characters; a shorter string is returned unchanged. It is not for digests or revisions that want a different length or a placeholder for empty (watch's shortRev, deploy's 7-character log sha) — those keep their own functions so their output never changes.
func Span ¶
Span words a duration: "just now" under a minute, then "12m", "3h 48m", "3 days", "5 weeks", "3 months", "2 years". Coarse on purpose — the reader is comparing builds, not timing them.
func StatusBar ¶
StatusBar renders one line exactly width cells wide: left text, then right text flush against the right edge. When both do not fit, left is truncated with an ellipsis so the key hints on the right stay visible; when even right alone does not fit, right is cut. Both arguments may carry ANSI styling; widths are measured on the visible text.
func StepGlyph ¶ added in v0.2.0
StepGlyph returns the one glyph set every step strip in this app should use (the audit doc): ✓ done, ◐ active, · pending, ✗ failed, ⏸ waiting. It retires the older, inconsistent ●/◍/○/⟳ glyphs different screens picked independently. The check that nothing drifts back is `git grep -n '"[●◍○⟳]"' internal/ui`, which must stay empty.
Types ¶
type Frame ¶
Frame is the shape every hoist screen is drawn in (AGENTS.md §4.8, M10): a titled, rounded box whose Sections are separated by rules, and a Footer — the status bar — that is always the terminal's last line no matter how tall the box is. The box is tight to its content; a screen that wants it to fill the terminal sizes its own section (a viewport, a table) to Frame.BodyHeight. Nothing here draws a box character by hand: the edges, the junctions and the title line all come from lipgloss's Border, so the widths are right by construction rather than by counting.
A screen's View is `ui.Frame{...}.Render(styles, width, height)`; a sub-pane inside a section (the in-flight panel under the matrix) is Box, the same thing without a footer — it lives inside the frame's own Sections rather than a separate Panes field below the box.
func (Frame) Render ¶
Render draws the frame for a width×height terminal: the box, then blank rows, then the footer on the last line. Content wider than the box is truncated with "…"; content taller than the space above the footer is trimmed — but never by dropping the closing border off the bottom, and never by deleting a screen's header (or any other earlier section) to keep a bare "…" marker for the LAST section alone. When the assembled box is too tall for the room above the footer, rows are cut from the bottom of the box's LAST section only — the section nearest the closing border, never an earlier one — down to a single dim "…" continuation row. If even THAT (its own rule included) is not enough to close the gap, the whole section is dropped outright instead of leaving an orphaned "…" that was never going to be enough on its own; the box then ends at whatever section is now last. Only as an absolute last resort — one that should never fire while any section still had something left to give — does an earlier row get dropped; the closing border itself is never a candidate for removal. A width or height too small to hold a box (under 4 columns or 3 rows) renders only what fits.
type StepState ¶ added in v0.2.0
type StepState uint8
StepState is one step's state in a pipeline strip (a promotion's branch/commit/push/…/rollout steps, a restart target's own progress) — the shape the flight screen and the matrix's in-flight rows share.
const ( // StepDone is a completed step. StepDone StepState = iota // StepActive is the one step currently running. StepActive // StepPending is a step not yet reached. StepPending // StepFailed is a step that ended in error. StepFailed // StepWaiting is a step blocked on something outside hoist (a human's approval comment, a // check run) rather than actively failed or simply not yet reached. StepWaiting )
type Styles ¶
type Styles struct {
// Dark records which background the palette was built for.
Dark bool
// Header styles a table header cell, Cell an ordinary cell, Selected the cursor row.
Header, Cell, Selected lipgloss.Style
// Status styles the status-bar summary, Notice a transient message shown in its place,
// Hint the key hints on the right of the bar.
Status, Notice, Hint lipgloss.Style
// Help styles the expanded help line toggled by ? — its own shade, distinct
// from Dim's grey (previously identical), so the two read as different things when a
// screen shows both at once (the help overlay's own body over a dimmed frame).
Help lipgloss.Style
// Chrome (M10). Border colours every frame and pane edge; Title the name in a frame's
// top edge; Rule a section divider's own text, when it carries one.
Border, Title, Rule lipgloss.Style
// Semantic colours, reinforcing a word rather than replacing it (the state is always
// spelled out; colour is the second channel). Good: pinned, green CI, "in staging".
// Warn: drifted, split, a migration, a blocked step. Bad: a failed step, an error line.
// Dim: external, an unreached step, "…10 more". Accent: the cursor, an id, a command
// the operator should type. Production: its own hue, distinct from Warn — a
// production target is a fact about the env, not a warning about the current change.
Good, Warn, Bad, Dim, Accent, Production lipgloss.Style
// Info is its own colour, split out of what used to be Notice/Warn/Production
// sharing one amber: a plain "this happened" report (a promotion started, a URL shown),
// never itself a warning or a failure. Notice keeps its own field and colour — the
// transient-message role frame.go's NoticeLines still defaults to — so existing callers
// are unaffected; Info is what a caller now picks explicitly to colour by kind (the
// root's activity row, AGENTS.md §4.8/§9 — ui.NoticeLinesStyled takes the style a caller
// chose instead of always reaching for Notice).
Info lipgloss.Style
// Cursor is the cell-cursor style the matrix's grid renderer needs: a background fill
// (not just a foreground colour, the way Selected reinforces a
// row) plus bold, so a single highlighted cell reads clearly against a table of many.
Cursor lipgloss.Style
// Add and Del colour the + and - lines of a diff.
Add, Del lipgloss.Style
}
Styles is the palette for one terminal background. The root model builds it once from tea.BackgroundColorMsg and pushes it into every screen; screens never pick colours themselves, so a theme change is one call.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package confirm builds the standalone huh.Confirm every screen raises inside a ui.Dialog.
|
Package confirm builds the standalone huh.Confirm every screen raises inside a ui.Dialog. |
|
Package keys is the one registry of the whole approved screen × key table (docs/audit/2026-09-ux-arch-audit.md, "Proposed keymap"), plus the two things every screen needs to act on it: a write-binding matcher that tells legacy-shift from caps lock (§9 entry 13), and the footer helper that lays out hints under §9 entry 10's "the frame owns every row" rule.
|
Package keys is the one registry of the whole approved screen × key table (docs/audit/2026-09-ux-arch-audit.md, "Proposed keymap"), plus the two things every screen needs to act on it: a write-binding matcher that tells legacy-shift from caps lock (§9 entry 13), and the footer helper that lays out hints under §9 entry 10's "the frame owns every row" rule. |
|
Package uitest is the one harness every screen's tests render through (AGENTS.md §4.8, M10): a golden comparison at a stated terminal size that also asserts the shape a terminal would actually show — exactly height lines, none wider than width — and a way to drive a screen with real keypresses, so a test can never pass by setting the field a key would have set.
|
Package uitest is the one harness every screen's tests render through (AGENTS.md §4.8, M10): a golden comparison at a stated terminal size that also asserts the shape a terminal would actually show — exactly height lines, none wider than width — and a way to drive a screen with real keypresses, so a test can never pass by setting the field a key would have set. |