Documentation
¶
Overview ¶
Package styles holds the shared lipgloss styles and glamour helpers for the LoopRig TUI. It is a leaf package: it depends only on charm libraries and must never import the tui package or any of its other subpackages.
Index ¶
- Constants
- Variables
- func DeriveBackgroundSGR(bg color.Color) (open, reset string)
- func FillLineBackground(line string, width int) string
- func FillLineBackgroundWith(line string, width int, open, reset string) string
- func NewMarkdownRenderer(width int) (*glamour.TermRenderer, error)
- func NoticeStyle(level uint8) lipgloss.Style
- func RenderMarkdown(r *glamour.TermRenderer, markdown string, width int) (string, error)
- func SelectedRow(row string, width int) string
- func SelectedRowWithRail(rail, body string, width int) string
- func ToolNode(s NodeStatus) string
- type NodeStatus
Constants ¶
const ( AccentBar = "▌" AccentBarPrompt = AccentBar + " " // WorkflowActivityMarker starts a new workflow-run notification rail. WorkflowActivityMarker = "●" // WorkflowActivityContinuationMarker continues an already-active workflow-run rail. WorkflowActivityContinuationMarker = "|" )
AccentBar is the left bar marker shared by user-message rows and the input prompt. AccentBarPrompt is the bar plus its trailing space, used as the prompt.
const Dot = "● "
Dot is the leading marker rendered before assistant/markdown blocks — the PLAIN layout form (the bullet glyph + a trailing space, dotWidth columns). The rendered bullet is COLORED via LitDot; Dot itself stays uncolored so it doubles as the width/layout reference (and the ANSI-stripped substring tests match against).
const RailColor = "#505050"
RailColor is the quiet dark-neutral foreground shared by normal-panel rails and the assistant-step timeline. It keeps structural markers visible without competing with the content or the status-colored node glyphs.
const SubagentCursor = "▸ "
SubagentCursor is the leading marker of a collapsed subagent activity line — the "▸ <agent>: <verb>" row attributing a subagent loop's StepDone to its agent. It reuses the same ▸ glyph as the prompt choice cursor (a "drill-in"/secondary marker), plus a trailing space.
const ThinkingHeader = "thinking"
ThinkingHeader labels the model's reasoning block.
Variables ¶
var ( MarkdownHeadingColor = "#A2D2FF" MarkdownInlineCodeColor = "#A2D2FF" MarkdownCodeNeutralColor = "#C4C4C4" )
Nexus markdown palette applied by NewMarkdownRenderer over glamour's DarkStyleConfig for non-code markdown, plus the base neutral shared with the code-block theme below. MarkdownHeadingColor replaces glamour's heading blue (ANSI 256 "39") and MarkdownInlineCodeColor replaces glamour's inline `code` red (ANSI 256 "203"), both with the same softer brand blue. MarkdownCodeNeutralColor is the base foreground for code text. They are hex strings (not lipgloss.Color) because glamour's StylePrimitive.Color is a *string.
var ( DiffDeletionBackgroundColor = lipgloss.Color("#4A2028") DiffAdditionBackgroundColor = lipgloss.Color("#203D2A") )
DiffDeletionBackgroundColor and DiffAdditionBackgroundColor tint changed rows in a permission-gate diff. Both are deliberately dark so syntax-highlighted foregrounds remain readable while the mutation direction is still visible at a glance.
var ( UserStyle = lipgloss.NewStyle().Bold(true) InterruptedStyle = lipgloss.NewStyle().Faint(true).Italic(true) StatusStyle = lipgloss.NewStyle().Faint(true) // StatusWorkingStyle / StatusWorkingAltStyle are the two phases of the status-line // icon while the model is actively working: lit (the assistant lime) and its blink // alternate (white). Waiting/thinking alternate between them on the blink tick for a // gentle pulse; streaming holds the lit lime. At rest / when blocked the icon falls // back to the faint StatusStyle. StatusWorkingStyle = lipgloss.NewStyle().Foreground(DotColor) StatusWorkingAltStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")) // QueuedStyle renders the transient queued-input affordance — the pending, // not-yet-running echo of a submitted user message shown below the live tail. It // is FAINT (not bold like UserStyle) so a queued line reads as a quieter "this is // waiting" hint, distinct from the bold committed user row it later promotes to. QueuedStyle = lipgloss.NewStyle().Faint(true) // QueuedLabelStyle renders the "QUEUED" banner shown ABOVE a queued message: the // brand blue (MarkdownHeadingColor, #A2D2FF), bold, so the pending state reads as a // clear labeled header distinct from the faint message preview beneath it. QueuedLabelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(MarkdownHeadingColor)).Bold(true) )
Role styles (exported so package tui can use them).
var ( NoticeInfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // neutral gray (user-message tone) NoticeWarnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("11")) // bright yellow NoticeErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // red )
Notice styles color a leveled notification's shared "▌ " accent bar (and text) by severity. All three reuse the SAME accent-bar wrapper as user messages and differ only in foreground color: info is a neutral gray (color 8), warn is bright yellow (color 11), error is red (color 9). They are selected per entry via NoticeStyle; callers must not branch on the level themselves.
var ( ToolCallStyle = lipgloss.NewStyle().Faint(true) // "└ ToolName Summary <glyph>" lines ToolResultStyle = lipgloss.NewStyle().Faint(true) // indented result-preview lines )
Tool-call styles: a tool card and its result preview render dim, subordinate to the assistant narration they nest beneath.
AccentBarStyle colors the left accent bar ("▌") on user rows, the startup banner, and unselected trays with the shared quiet dark neutral (RailColor, #505050).
var BoxStyle = lipgloss.NewStyle(). Border(composerBorder, false, false, false, true). BorderForeground(InputAccent). PaddingLeft(1)
BoxStyle frames the composer (input) box as the lowest-footprint affordance: a left-only ▌ accent edge + one space of left padding, and NOTHING else — no border box, no background fill, no top/bottom padding. This is deliberate: the bubbletea v2 inline renderer strands the composer's rows into scrollback when a resize desyncs its relative cursor, and the loudness of that artifact scales with how much each row paints. A single, full-width-tinted or bordered row strands as a bar/box; a bare "▌ text" row strands as a faint one-glyph fragment. So the box paints no full-width glyphs and occupies the fewest rows possible (just the editor's content lines). The editor renders to the right of the edge; callers subtract the horizontal frame (edge + left padding = 2 cols) from the box width to size the inner textarea.
var CardBorderColor = lipgloss.Color(MarkdownHeadingColor)
CardBorderColor tints the gate card's left ▌ rail — and the pressable accelerator keys in its footer — a soft brand blue (the same calm accent as MarkdownHeadingColor). A pending permission/AskUser gate is an action-required FOREGROUND affordance, so it reads as a blue-tinted panel card (the user-message card treatment in blue), distinct from the faint, borderless tool cards and the neutral-gray composer, and stays legible on a dark terminal.
var CardHintStyle = lipgloss.NewStyle().Faint(true)
CardHintStyle renders a card's muted secondary text — a key hint's label ("once"), the choice key legend, the free-text submit hint, and the "(+N more pending)" queue-depth note — faint so it recedes beneath the title and body.
var CardKeyStyle = lipgloss.NewStyle().Bold(true).Foreground(CardBorderColor)
CardKeyStyle renders the bracketed accelerator of a footer key hint ("[y]", "[o]") in the bold brand accent, so the pressable key stands out from its muted label.
var CardPanelBg = lipgloss.Color("#242527")
CardPanelBg is the fill painted behind a gate card — a near-neutral dark gray carrying only a whisper of blue (the blue tint dialed ~90% out of the earlier #1b2233), so a pending gate reads as the same padded card family as the neutral user-message panel (PanelBg) with just enough cool cast to pair with the blue rail. Dark enough that the bold title, body text and dim hints stay readable on a dark terminal.
var CardRailStyle = lipgloss.NewStyle().Foreground(CardBorderColor)
CardRailStyle colors the gate card's left ▌ rail the brand blue, matching the user card's gray rail but tinted — the continuous accent edge down the padded blue panel.
var CardSelectedBg = lipgloss.Color("#2c3a5a")
CardSelectedBg highlights the selected choice row — a brighter blue than CardPanelBg so the current selection reads as a filled bar standing proud of the panel (the opencode dialog look), rather than only the ▸ cursor.
var CardSelectedStyle = lipgloss.NewStyle().Bold(true).Background(CardSelectedBg)
CardSelectedStyle highlights the selected choice row: bold with the brighter blue fill (CardSelectedBg) spanning the row, so the current selection reads as a filled bar rather than only the ▸ cursor. The caller sets the row Width so the fill spans the card body.
var CardTitleStyle = lipgloss.NewStyle().Bold(true)
CardTitleStyle renders the bold card title row — "Approve Bash?", the AskUser question, or the "answer" label — so the required action reads at a glance above the card body.
var CurrentChoiceStyle = lipgloss.NewStyle().Foreground(CardBorderColor)
CurrentChoiceStyle marks the value that is active behind a picker. It is deliberately separate from the cursor's selection band: after the cursor moves, this blue primary remains on the live value while the band shows what Enter would choose.
var DotColor = lipgloss.Color("#D4F84D")
DotColor is the assistant bullet's foreground color.
var FailColor = lipgloss.Color("#FF6B6B")
FailColor tints a failed rail node and its header. Kept distinct from DotColor so a failure reads red against the neon assistant bullet.
var HeadlineStyle = lipgloss.NewStyle().Bold(true)
HeadlineStyle renders the bold headline word shown beside the assistant dot for an empty-text tool step (the live "working" synonym and the committed "Done") — design §3 rule 4. Bold so the headline stands out beside the bullet, matching the bold emphasis the user message and prompt headers use.
var InputAccent = lipgloss.Color(RailColor)
InputAccent colors the composer's left ▌ edge — the same quiet dark neutral as AccentBarStyle, so every normal panel keeps one shared rail color.
var LitDot = lipgloss.NewStyle().Foreground(DotColor).Render("●") + " "
LitDot is the COLORED leading marker actually rendered before an assistant bullet: the DotColor-foregrounded glyph plus a plain trailing space. Its display width equals Dot's (the color is zero-width ANSI), so narration alignment is unchanged.
var PanelBg = CardPanelBg
PanelBg is the shared darker-gray fill behind MODERN mode's user-message rows, composer panel, and unselected completion-tray rows. It aliases CardPanelBg rather than spelling another hex value so the three related surfaces cannot drift apart. MODERN-ONLY: the scrollback rows and the scrollback composer never paint a background (see BoxStyle's rationale) — the fill is safe in the alt-screen viewport because it re-renders the whole frame each tick.
var PromptCursorStyle = lipgloss.NewStyle().Bold(true)
PromptCursorStyle renders the ▸ cursor marking the selected choice row, bold so the selection stands out from the unselected rows.
var PromptHeaderStyle = lipgloss.NewStyle().Bold(true)
PromptHeaderStyle renders a prompt box's header label (e.g. "Approve Bash?"), bold so the action reads at a glance above the body.
var PromptHintStyle = lipgloss.NewStyle().Faint(true)
PromptHintStyle renders a prompt box's faint secondary hints — the key legend ("↑/↓ select · …") and the "(+N more pending)" queue-depth note.
RailStyle renders timeline spines and connectors independently from ThinkingStyle, allowing the rail to recede without also reducing the readability of reasoning text.
var SubagentStyle = lipgloss.NewStyle().Faint(true)
SubagentStyle renders a collapsed subagent activity line ("▸ <agent>: done"). It is FAINT so a subagent's collapsed-but-present step reads as quieter, subordinate chatter beneath the primary (orchestrator) narration — matching the faint tool-card and queued-affordance tone, distinct from the bold primary user/assistant rows.
var ThinkingStyle = lipgloss.NewStyle().Faint(true)
ThinkingStyle renders the model's reasoning block: faint (never italic), subordinate to the assistant narration it precedes. Italic is deliberately omitted — it skewed the "│ " left rail and broke the column alignment; a non-italic rail renders as a clean, unbroken vertical line.
var UserBgStyle = lipgloss.NewStyle().Background(PanelBg)
UserBgStyle is the background-only style the shared darker-gray fill is derived from (its SGR open/close pair). It carries no foreground, so a filled row's own accent bar and markdown keep their colors on top of the fill.
var WorkflowActivityStyle = lipgloss.NewStyle().Foreground(CardBorderColor)
WorkflowActivityStyle is the shared TUI blue used for durable workflow markers. It intentionally reuses CardBorderColor so notifications and action cards have one semantic blue token rather than drifting shades.
Functions ¶
func DeriveBackgroundSGR ¶
DeriveBackgroundSGR returns the SGR pair that turns a background fill of color bg on (open) and off (reset). It renders a NUL sentinel through a background-only style (lipgloss emits open + NUL + reset) and splits on the sentinel, so no escape is ever hardcoded. A caller filling many lines with one color derives the pair once and passes it to FillLineBackgroundWith. A degenerate result (open == "") signals the caller to skip filling (fail-safe).
func FillLineBackground ¶
FillLineBackground paints the shared darker-gray PanelBg fill behind one already-styled line to width display columns — the convenience over FillLineBackgroundWith for the one shared fill. See FillLineBackgroundWith for the fill semantics. MODERN-ONLY: scrollback never fills a background (see BoxStyle).
func FillLineBackgroundWith ¶
FillLineBackgroundWith paints the fill whose SGR pair is (open, reset) behind one already-styled line so it spans width display columns: it opens the fill, RE-OPENS it after every inner SGR reset (glamour and lipgloss end their spans with full resets that would otherwise punch holes in the fill — a plain Background() wrap only tints up to the first reset), pads the visible content out to width with fill-carrying spaces, and closes with a reset. An empty open returns the line unchanged (fail-safe: a degenerate derivation must never emit a broken escape). A width at or below the line's own display width adds no padding (the fill still spans the content). It is the shared primitive behind MODERN mode's darker-gray user rows and composer panel.
func NewMarkdownRenderer ¶
func NewMarkdownRenderer(width int) (*glamour.TermRenderer, error)
NewMarkdownRenderer builds a glamour renderer for the given wrap width.
It uses the static DarkStyleConfig deliberately — never glamour.WithAutoStyle(). Auto style calls termenv.HasDarkBackground(), which writes an OSC-11 background query plus a CSI-6n cursor probe to the terminal and reads the replies back off stdin. Inside a Bubble Tea program — which owns stdin in raw mode — those replies race the input reader and (a) leak into the UI as stray bytes like "]11;rgb:…" and "[…;…R", (b) desync the renderer's cursor tracking, and (c) stall the render loop. The static config does no terminal I/O.
The document's left margin is zeroed so narration aligns flush under the "●" bullet that package tui prepends (otherwise glamour indents every line by 2).
The Nexus palette is applied to glamour's defaults: markdown headings (glamour's blue) and inline `code` spans (glamour's red) are recolored to MarkdownHeadingColor and MarkdownInlineCodeColor. The inline `code` span additionally drops glamour's dark background fill and its U+00A0 prefix/suffix (which padded each span with a leading/trailing space), so a `code` span renders as bare colored text. The H2–H6 heading prefixes (glamour's literal "## ", "### ", … markers) are cleared so a heading renders as clean styled text rather than echoing its markdown hashes; H1 keeps its colored background bar (it never carried a "#" marker).
Code-block syntax highlighting uses the Nexus theme outright: cfg.CodeBlock.Chroma is pointed at the package-level nexusChroma rather than inheriting glamour's reds-laden DarkStyleConfig chroma and patching it token by token. See nexusChroma for the palette and the rationale (this is why box-drawing file trees no longer render on a red background, and why path slashes, arrows and +/- markers are not red).
cfg is a value copy of DarkStyleConfig, so reassigning its (non-pointer) fields never mutates the shared package-level config (the same copy-then-override pattern as the document margin).
Returns an error if glamour fails to construct (caller decides fallback).
func NoticeStyle ¶
NoticeStyle maps a notice level (0=info, 1=warn, 2=error) to its style. An unknown level falls back to the neutral info style (fail-safe: an unrecognised level must never panic or pick an alarming color). The level argument is a plain uint8 so the leaf styles package need not import package tui's noticeLevel type (it depends only on charm libraries — see the package doc).
func RenderMarkdown ¶
RenderMarkdown keeps the TUI's responsive table policy outside Glamour while preserving Glamour's exact output for documents whose tables remain readable.
func SelectedRow ¶ added in v0.17.0
SelectedRow bands row with the shared selection fill across width columns. There is deliberately NO cursor glyph — the band is the cursor.
It takes the row in ONE form. On a dark fill the row keeps its own styling (FillLineBackgroundWith re-opens the fill after every inner SGR reset); on a light fill the styling would wash out, so the row is stripped and re-rendered near-black. The bold-blue accelerator therefore loses its blue on the SELECTED row only — accepted, since against an identical fill it would be invisible anyway. Callers pass one string and so cannot get a styled and a plain form out of sync.
It PADS to width but never TRUNCATES: a row wider than width comes back whole. Cutting a row to the surface width is the caller's job (the tray ansi.Truncates first), because only the caller knows what a sensible tail looks like.
If the fill derives no SGR (a degenerate or nil color) there is no band to paint and the row is returned untouched — no fill, but still legible. Restyling it near-black without a band behind it would render it black-on-black.
func SelectedRowWithRail ¶ added in v0.18.0
SelectedRowWithRail bands rail+body exactly as SelectedRow bands a row, except that rail -- a panel's left edge glyph -- is drawn in the FILL's color instead of the row's text color, so the edge runs unbroken down the panel and disappears into the band on the selected row.
It is the general form: SelectedRow is this with an empty rail, so the two can never drift.
func ToolNode ¶
func ToolNode(s NodeStatus) string
ToolNode returns the COLORED node glyph (glyph + trailing space, 2 columns wide like LitDot) for a tool/subagent node at the given status.
Types ¶
type NodeStatus ¶
type NodeStatus uint8
NodeStatus selects a rail node's tint. A hollow-circle tool/subagent node is lime when OK, red when failed, and a pulsing filled glyph while running.
const ( NodeOK NodeStatus = iota NodeFailed NodeRunning )