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 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 ( 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 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 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 )