theme

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package theme is the shared visual language of Sneat Co., DataTug and FileTug terminal applications: colours, card framing, and chrome (bars, composer frame, panel rows) that every product renders through, so all of them look and behave alike with no styling code of their own. The chat surface of strongo/aichat and the navigation shell and widgets of tuigoff draw exclusively with it.

The package was born in strongo/aichat as tui/theme and moved here so that aichat depends on tuigoff and not the other way round.

Founder ruling (2026-09-25): "UI styling should be unified across apps. Message should be like a card in chat of any app." and (same day, follow-up): "Same for composer block and hints status bar and top menu and side panel." DataTug chat's look (pkg/chat/chatui.go's topBar/ statusBar/renderMarkdown and its transcript entry styling) was the reference; this package is where that look now lives, as the DEFAULT — a product supplies only CONTENT (title, hint labels, menu items, message text); every colour, border, and padding decision lives here, once.

No other package should construct a lipgloss.NewStyle() with a hard-coded colour literal — read a colour or a render helper from here instead, so changing the look means editing this one package.

Index

Constants

View Source
const CardPaddingCols = 2

CardPaddingCols/Rows is the space, in columns/rows, kept between a card's fill edge and its content.

View Source
const CardPaddingRows = 1
View Source
const MarginCollapseRows = 24

MarginCollapseRows is the terminal row count AT OR BELOW which ContentMargins returns 0 — founder: "collapse both below 24 terminal rows".

View Source
const MarginRows = 1

MarginRows is the number of blank rows chatshell inserts at each of the two margin points above, when the terminal is tall enough (see ContentMargins) — a single shared constant so every product's spacing agrees, per this package's own no-per-product-styling rule.

View Source
const MarkerColumnWidth = cardBarWidth

MarkerColumnWidth is the 1-column focus-marker gutter every Card/ ComposerFrame reserves to the left of its own surface (see surfaceFill's cardBarWidth/composerBarWidth) — blank when unfocused, so a card/composer's SURFACE (its own drawn left edge, "▄"/"▀"/fill) always starts in the SAME column, terminal column MarkerColumnWidth, whether focused or not. A SelfFramed transcript Block (tui/grid — the one exception to the Card wrap, see transcript's own SelfFramed doc) draws its OWN border directly rather than going through Card, so it must reserve this same gutter itself to keep its border lined up with every other surface's left edge — see ReserveMarkerColumn.

View Source
const MaxInlineGridRows = 10

MaxInlineGridRows is the shared default for how many data rows a grid shows at once when embedded inline in a transcript (tui/grid.Model's DefaultMaxVisibleRows) — founder 2026-09-25: "Grids embedded in the chat transcript show at most 10 data rows (default; make it a theme/ transcript constant ... overridable per product via an option, not a per-product style)". A product overrides it per grid via grid.WithMaxVisibleRows(n), never by redefining this constant.

Variables

View Source
var (
	Black                = lipgloss.Color("#000000")
	White                = lipgloss.Color("#FFFFFF")
	WhiteSmoke           = lipgloss.Color("#F5F5F5")
	Gray                 = lipgloss.Color("#808080")
	Grey                 = Gray
	DarkGray             = lipgloss.Color("#A9A9A9")
	DarkSlateGray        = lipgloss.Color("#2F4F4F")
	LightGray            = lipgloss.Color("#D3D3D3")
	LightGrey            = LightGray
	Red                  = lipgloss.Color("#FF0000")
	DarkRed              = lipgloss.Color("#8B0000")
	PaleVioletRed        = lipgloss.Color("#DB7093")
	LightCoral           = lipgloss.Color("#F08080")
	LightSalmon          = lipgloss.Color("#FFA07A")
	Orange               = lipgloss.Color("#FFA500")
	Gold                 = lipgloss.Color("#FFD700")
	Yellow               = lipgloss.Color("#FFFF00")
	LightYellow          = lipgloss.Color("#FFFFE0")
	LightGoldenrodYellow = lipgloss.Color("#FAFAD2")
	Green                = lipgloss.Color("#008000")
	Lime                 = lipgloss.Color("#00FF00")
	Cyan                 = lipgloss.Color("#00FFFF")
	Blue                 = lipgloss.Color("#0000FF")
	DarkBlue             = lipgloss.Color("#00008B")
	CornflowerBlue       = lipgloss.Color("#6495ED")
	LightBlue            = lipgloss.Color("#ADD8E6")
	LightSteelBlue       = lipgloss.Color("#B0C4DE")
	Magenta              = lipgloss.Color("#FF00FF")
)

Named colours use the X11 names and values. They are fixed hues for text that must not follow the light or dark variant, such as a cell of a given data type or a node of a given kind.

View Source
var (
	// TableColumnTitle colours the title of a table column.
	TableColumnTitle color.Color = LightBlue
	// TableTertiaryText colours de-emphasised table text.
	TableTertiaryText color.Color = Gray
	// TableHeaderColor colours a table header row.
	TableHeaderColor color.Color = WhiteSmoke
	// TreeNodeLink colours a tree node that navigates somewhere.
	TreeNodeLink color.Color = Blue
)

Semantic colours of data screens. They are fixed hues; use FocusColor, MutedColor and AccentColor for colours that must follow the terminal background.

View Source
var Dark = true

Dark selects the theme variant: true (the default) means the terminal has a dark background. Every colour helper in this package reads it at call time (not once at init), so SetDark takes effect on the very next render — e.g. a product toggling a light/dark preference at runtime, or a tool that renders the same screen once per variant (see cmd/mdrender-snapshot- style tooling in downstream products).

View Source
var HalfBlockEdges = true

HalfBlockEdges is the product-facing on/off switch (default true) -- SetHalfBlockEdges(false) opts a product out entirely, independent of colour-profile detection (e.g. a product that knows its target terminal renders half-blocks with a visible seam despite TrueColor support).

Functions

func AccentColor

func AccentColor() color.Color

AccentColor is the shared emphasis colour for a hint's key/shortcut (distinct from FocusColor, which marks focus/selection specifically).

func Bar

func Bar(width int, content string) string

Bar pads content to width and fills the remainder with the shared chrome background, truncating with an ellipsis if content overflows — so a bar's right edge always reaches the terminal edge, whatever a product supplied. content may already contain nested lipgloss-styled spans (e.g. TopBar's per-item styling); Bar only sets the background/width frame and the HintsInset() horizontal padding around it, not a foreground override, so those spans' own colours survive. Used by TopBar only — see StatusLine for the hints/status bar's own (backgroundless) chrome.

func BlueText

func BlueText(s string) string

BlueText returns s coloured blue.

func BorderColor

func BorderColor(focused bool) color.Color

BorderColor returns FocusColor() when focused, MutedColor() otherwise — the one rule every bordered element (a Card, the composer frame, a grid's own card, a panel frame) follows for its border colour, so a product never has to decide this for itself.

func Card

func Card(role Role, header, body string, width int, focused bool) string

Card renders body (and, when non-empty, header above it in bold) as a filled, coloured background block for role, at OUTER width — see the package doc above for the no-border design and its focus/selection language. Every product's message/Block cards render through this one function.

func ChipCloseColor

func ChipCloseColor(bg color.Color) color.Color

ChipCloseColor returns the colour a chip's "×" close glyph reads in — "muted, but still >= 4.5:1" (founder 2026-09-25, r10 coordinator review): MutedColor() itself whenever it clears bodyTextMinRatio against bg, otherwise the SAME full-contrast colour the chip's own label text uses (surfaceText(bg)) — MutedColor() is a fixed pair picked for the ORIGINAL (weaker) surface tints; a stronger chip fill can push it under 4.5:1 for a given real terminal background, and this guarantees compliance either way rather than assuming it always holds.

func ChipColors

func ChipColors() (bg, fg color.Color)

ChipColors returns an attachment chip's UNFOCUSED fill: ComposerColors' own background blended chipTintAmount further toward the block hue, with a contrast-safe foreground (surfaceText) — label text and the "×" close glyph both read this pair, so both clear bodyTextMinRatio (verified by ContrastPairs' chip entries).

func Colorize

func Colorize(s string, color ANSIColor) string

Colorize wraps s in color and the reset sequence set by SetDefaultColor. The result can be placed inside any text that is rendered through a component.

func ComposerChipLeadingFill

func ComposerChipLeadingFill() int

ComposerChipLeadingFill returns how many "▄" edge-filler columns belong between the composer's marker column and the first attachment chip's own cell, so that chip's LABEL — which starts 1 column into the cell, after the cell's own leading inner-padding space — lands exactly on ComposerTextColumn(). It also guarantees the composer's own top-left corner (marker + at least one edge-filler column) is always rendered before any chip, never overdrawn by one — founder, r11: "Attachment chips should have margin on left so left top corner is always rendered."

func ComposerChipMarker

func ComposerChipMarker(focused bool) string

ComposerChipMarker renders the same focus-marker cell (see markerCell) the composer's own edges show, for a caller (chatshell's chip strip) that draws the composer's top edge itself when chips are present.

func ComposerColors

func ComposerColors() (bg, fg color.Color)

ComposerColors returns the composer's UNFOCUSED fill: TerminalBackground() blended composerTintAmount toward the block hue, with a contrast-safe foreground (surfaceText) — see the package doc on colorsFor for why this derives from the REAL/guessed terminal background rather than a fixed hex pair.

func ComposerFocusColors

func ComposerFocusColors() (bg, fg color.Color)

ComposerFocusColors returns the composer's FOCUSED fill: ComposerColors' own unfocused background blended composerFocusTint of the way toward FocusColor() (foreground unchanged — the blend is subtle enough that ComposerColors' foreground still meets bodyTextMinRatio against it; verified by TestContrastMeetsWCAG's composer pairs).

func ComposerFrame

func ComposerFrame(width int, content string, focused bool) string

ComposerFrame wraps a composer's rendered input view in the shared filled background: SurfaceColors unfocused, ComposerFocusColors (a subtle tint toward FocusColor(), not the full bright fill a Card uses) plus a left accent bar in FocusColor() while focused — so "the composer has focus" reads primarily from the accent bar, with the fill only nudged, never a "bright slab". content is run through paintOver first: a bubbles input's own View() carries its own ANSI styling (cursor cell, etc.) including its own resets, which would otherwise cut this fill's background off partway through the line (the "lost composer background" regression — see the paintOver doc above).

func ComposerFrameNoTopEdge

func ComposerFrameNoTopEdge(width int, content string, focused bool) string

ComposerFrameNoTopEdge is ComposerFrame without its own top edge row — for when a caller is rendering something ELSE immediately above that already performs the top edge's job visually (chatshell's chip strip, see chip.go's chipsEdgeRow: founder, r9, "have attachment chips in the top line of the composer ... the chips row that currently sits inside the composer moves here"). In FALLBACK mode (HalfBlockEdgesActive() false), there is no separate "edge row" concept to omit, so this is identical to ComposerFrame — the caller's own chip row still renders as its own line above, exactly as before this feature existed.

func ComposerFrameSize

func ComposerFrameSize() (cols, rows int)

ComposerFrameSize returns how many extra columns/rows ComposerFrame adds around its content, so a caller (chatshell's resize/historyHeight) can size the inner input and reserve the right amount of screen space.

func ComposerTextColumn

func ComposerTextColumn() int

ComposerTextColumn returns the absolute column (0-based, from the composer's own left edge — the marker column) where the composer's typed text / placeholder starts: the marker column plus the left padding. A caller drawing content ABOVE the input on the composer's own surface (chatshell's chip strip) uses this to line its own content up with that same column — founder, r11 (Warp feedback, verbatim): "I think first chip text should be aligned with text of the message."

func ContentMargins

func ContentMargins(terminalRows int) int

ContentMargins returns MarginRows when the terminal is taller than MarginCollapseRows, 0 otherwise — the one function chatshell calls at both margin points (top-bar/content, and last-card/composer) so the collapse rule lives in exactly one place.

func Contrast

func Contrast(a, b color.Color) float64

Contrast computes the WCAG 2.x contrast ratio between two colours — (L1+0.05)/(L2+0.05) with L1 the lighter relative luminance — the metric ContrastPairs' MinimumRatio thresholds are expressed in.

func ContrastText

func ContrastText(bg color.Color) color.Color

ContrastText is surfaceText, exported for a caller that pairs text with a background OTHER than one of this package's own derived surfaces — e.g. tui/grid's "highlighted but unfocused" row, which used to pair a role surface's own foreground with MutedColor() as the background: a combination that's only safe in ONE Dark variant (MutedColor() itself flips light/dark, but a role surface's foreground didn't flip to match), which is what produced the r10 regression — a dark grid row text on a dark MutedColor() background in light mode, unreadable. Any caller pairing text with an arbitrary/foreign background should read its colour from here instead of guessing.

func Danger

func Danger(s string) string

Danger returns s in the danger colour (red).

func Detect

func Detect() (isDark bool)

Detect reports whether the current terminal (os.Stdin/os.Stdout) appears to have a dark background, via lipgloss.HasDarkBackground. It never panics — a non-terminal (piped output, CI, a test) simply reports the package's built-in default (true) — so a product can safely call SetDark(Detect()) once at startup without special-casing non-TTY runs.

func ErrorColor

func ErrorColor() color.Color

ErrorColor is the colour of error text: a red that reads on both variants.

func FocusColor

func FocusColor() color.Color

FocusColor is the one accent colour used everywhere focus/selection is shown: a focused transcript card's border, the composer's border while it holds keyboard focus, a selected sidebar/panel row, and the active zone's border in general — one accent, so "what has focus" always reads the same way regardless of which chrome is showing it.

func FocusSurfaceColors

func FocusSurfaceColors() (bg, fg color.Color)

FocusSurfaceColors returns the SINGLE background+foreground pair every component uses to mark "this is focused/selected": background is FocusColor() itself — the SAME accent a focused Card's border and the composer's focused border use — paired with a foreground guaranteed to contrast with it in both Dark variants. A grid's highlighted row, a sidebar's selected row, and a join block's chosen candidate all use this pair, so "what is selected" reads as the one consistent accent across every component (founder 2026-09-25: "Use the single theme focus/ selection colour everywhere").

func GrayText

func GrayText(s string) string

GrayText returns s coloured gray.

func GreenText

func GreenText(s string) string

GreenText returns s coloured green.

func HalfBlockEdge

func HalfBlockEdge(width int, surfaceBG color.Color, top bool) string

HalfBlockEdge renders ONE half-block edge row, width cells wide: "▄" (foreground=surfaceBG) for the TOP edge — the surface's fill appears to start half a line in — or "▀" (same foreground) for the BOTTOM edge. Deliberately sets NO background at all (SGR stays at the terminal's own default, "49") — founder 2026-09-25 (r10 coordinator review, verbatim): "Edge rows and any 'terminal background' cells must NOT set a background at all (default bg, SGR 49) — only the fg = surface colour on the ▄/▀ glyphs." Painting an explicit TerminalBackground() guess here was the r10 regression: on a real terminal whose background differs from that guess (a Warp theme, Solarized, ...), it showed as a visibly wrong-coloured band above/below every card and the composer. Leaving the background cell UNSET lets the real terminal's own background show through exactly, always correct by construction — no guess needed for the glyph's own "off" half at all. Exported so a caller building its own leading/trailing fill around embedded content (chatshell's chip strip) can match Card/ComposerFrame's own edge glyph/colour rule exactly, rather than re-deriving it.

func HalfBlockEdgesActive

func HalfBlockEdgesActive() bool

HalfBlockEdgesActive reports whether Card/ComposerFrame will actually render half-block edges for the CURRENT call: HalfBlockEdges AND a TrueColor colour profile. Exported so a caller composing its own content around a Card/ComposerFrame (chatshell's chip strip, see chip.go) can match its own rendering choice to whichever mode is active.

func HeaderFor

func HeaderFor(role Role) string

HeaderFor returns the default role label a card shows above its body, e.g. "You" for RoleUser. Pass a non-empty header to Card to override it (e.g. a Block's own title); "" for RoleBlock (a Block untitled by default — see transcript.Titled).

func Hex

func Hex(c color.Color) string

Hex renders c as a "#rrggbb" string — for a caller that needs a plain hex literal rather than a color.Color (e.g. mdrender's own glamour ansi.StyleConfig, whose StylePrimitive.Color is a *string), so it can still read every colour from this package rather than hard-coding one of its own (this package's own no-hard-coded-literal rule, applied to an external library's string-typed config).

func HintsInset

func HintsInset() (left, right int)

HintsInset returns the LEFT/RIGHT column padding TopBar keeps from the terminal edge — the SAME columns a Card's own text keeps (cardBarWidth+CardPaddingCols on the left — where a card's own marker column would sit, plus its padding; CardPaddingCols on the right) — so the top bar's title starts in the same column a card's text does (founder, r12, verbatim: "[status line] Should have horizontal padding", coordinator's own restatement: "align its first key with the card text column, same right inset"; item 3, same round: give the top bar the same padding for the same alignment). SUPERSEDED for the hints/status bar itself, r14 (founder, verbatim: "Status panel should be aligned with composer border, not composer text") — see StatusInset, which StatusLine now uses instead.

func InnerWidth

func InnerWidth(width int) int

InnerWidth returns the content width available inside a card of the given OUTER width — what a Block should render at (via theme.InnerWidth(width)) so its own View(width, focused) output lines up exactly with the card Card(...) then wraps it in.

func MutedColor

func MutedColor() color.Color

MutedColor is the shared low-emphasis text colour (hint labels, an empty sidebar's placeholder, unfocused chrome borders).

func PaintOver

func PaintOver(content string, bg, fg color.Color) string

PaintOver is paintOver, exported for a product's OWN chrome that composites pre-styled nested content (e.g. its own row highlighting) onto a themed background outside of Card/ComposerFrame/PanelFrame/Bar — a product should reach for this instead of re-deriving the same nested- reset fix locally (see the package doc above these two functions).

func PanelColors

func PanelColors() (bg, fg color.Color)

PanelColors returns the side panel's own UNFOCUSED surface background+ foreground pair — SurfaceColors() in every way but its own named constant (panelTintAmount), so a change to one never silently retunes the other even though they start out equal.

func PanelFocusColors

func PanelFocusColors() (bg, fg color.Color)

PanelFocusColors returns the side panel's FOCUSED surface pair: its own unfocused bg (PanelColors()) blended a further panelFocusTint toward the block hue — see panelFocusTint's own doc. Foreground is re-derived (surfaceText) against the NEW bg, so it always clears bodyTextMinRatio regardless of the tint shift.

func PanelFrame

func PanelFrame(width, height int, header, content string, focused bool) string

PanelFrame renders side-panel content (SidePanel or the default sidebar) as a single FULL-HEIGHT card surface, spanning EXACTLY height rows of content (PanelFrameSize's own row overhead is added on top of that, same as Card/ComposerFrame) — founder, r12 (Warp feedback, verbatim): "side panel should be full height card." It reuses the surfaceFill machinery Card/ComposerFrame do for the fill itself and the half-block top/bottom edges when HalfBlockEdgesActive() — but, unlike Card/ComposerFrame, reserves NO focus-marker column at all (barWidth 0): founder, r13, verbatim: "Side panel should NOT have left accent border treatment... just slightly change background" — focused vs unfocused is PanelFocusColors() vs PanelColors() instead (see their own docs). header renders bold as the surface's own first content row (when non-empty); any row of the surface past what header+content actually fill is left blank SURFACE, never terminal background — founder, r12: "the empty space below its content filled with the panel surface". content itself supplies only its OWN per-row styling (e.g. tui/sidebar's SelectedRow highlight, which still uses the single shared FocusSurfaceColors() selection accent) — the surrounding fill is entirely PanelFrame's job, including panelPaddingCols' own left/right text inset (surfaceFill's paddingCols, same as Card's — 2026-09-25 fix: content used to be laid out flush against the surface's own first/last column). A single blank column of plain terminal background sits before the surface (panelGapWidth) — the divider between the chat column and the panel.

func PanelFrameSize

func PanelFrameSize() (cols, rows int)

PanelFrameSize returns how many extra columns/rows PanelFrame adds around its content, so a caller (chatshell's panel sizing) can size the panel's own content and reserve the right amount of screen space — mirroring ComposerFrameSize. cols is the one gap column (panelGapWidth) plus panelPaddingCols on BOTH sides of the content. rows is a CONSTANT 2 regardless of HalfBlockEdgesActive() — the SAME vPad-vs-edges equalisation ComposerFrameSize relies on (see surfaceFill's own doc): 1 padding row top+bottom in fallback mode, or the top/bottom half-block edge rows in half-block mode — either way exactly 2 extra rows, so a caller's row budget never has to branch on which mode is active.

func PanelHeader

func PanelHeader(title string) string

PanelHeader renders a side-panel/sidebar title header, e.g. "Sidebar" or a product workspace pane's tab name.

func RedText

func RedText(s string) string

RedText returns s coloured red.

func RenderHints

func RenderHints(width int, hints []Hint, segments ...string) string

RenderHints renders the shared hints/status bar: any trailing segments (e.g. a product/session summary or a hyperlink) first, then each Hint as its key (in AccentColor, bold) followed by its label (in MutedColor) — DataTug's original statusBar layout, now the default for every product. Unlike a single-line Bar, RenderHints WRAPS: a segment or hint that would overflow the current line starts a new line instead of being silently truncated (ported from DataTug's own wrapStatusSegments, now shared — founder 2026-09-25: "Same for ... hints status bar"), each wrapped line rendered through Bar so every line keeps the shared chrome. A product supplies only the hint list and segment strings; chatshell. WithHintsProvider (or the plain SetStatus default) wires this in automatically.

func ReserveMarkerColumn

func ReserveMarkerColumn(content string) string

ReserveMarkerColumn prefixes every line of content — already rendered at width-MarkerColumnWidth columns — with a blank MarkerColumnWidth- column gutter, producing a block exactly width columns wide whose own first drawn column (content's own column 0) lands in terminal column MarkerColumnWidth, the same column a Card/ComposerFrame's own surface starts in. It never draws a marker glyph itself — a SelfFramed block's own focus signalling (e.g. tui/grid's border colour) is unaffected; this only shifts position (founder correction, 2026-09-25: "if a focused grid currently signals focus only by border colour, keep that; just align it").

func SelectedRow

func SelectedRow(text string, selected bool, width int) string

SelectedRow renders one side-panel/sidebar row: " text" unselected, or a FocusSurfaceColors()-highlighted "› text" when selected — the SAME accent a focused card's border and a grid's highlighted row use, so a selected list row, a focused message, and a selected grid row all read as the same kind of thing (founder 2026-09-25: "Use the single theme focus/ selection colour everywhere"). width is the row's own full inner width (the same width its text was laid out at) -- selected passes it to Width() so the highlight fill spans every column of the row, not just as many as "› "+text happens to need (2026-09-25 fix: a short row previously left its own highlight looking like a narrow chip instead of a full-width selected bar, the same "surface spans identical columns on every row" rule Card/ PanelFrame already follow).

func SelectedStyle

func SelectedStyle(focused bool) lipgloss.Style

SelectedStyle styles the selected row, cell or node: the shared focus accent while the component holds focus, a quieter surface tint while it does not.

func SetColorProfileDetector

func SetColorProfileDetector(detect func() colorprofile.Profile) (restore func())

SetColorProfileDetector replaces colour-profile detection and returns a restore function. colorprofile.Env reports NoTTY when stdout is not a terminal, so COLORTERM=truecolor alone does not activate half-block edges under go test. Pass nil to restore detection from the process environment.

func SetDark

func SetDark(dark bool)

SetDark sets the theme variant explicitly, overriding auto-detection.

func SetDefaultColor

func SetDefaultColor(color ANSIColor)

SetDefaultColor sets the sequence appended after coloured text to restore the foreground colour.

func SetHalfBlockEdges

func SetHalfBlockEdges(v bool)

SetHalfBlockEdges sets HalfBlockEdges explicitly.

func SetTerminalBackground

func SetTerminalBackground(c color.Color)

SetTerminalBackground records the terminal's ACTUAL reported background colour and derives Dark from ITS luminance — founder 2026-09-25 (r10 coordinator review, verbatim): "derive surface tints from the ACTUAL terminal background ... Fallback to the current defaults when the terminal doesn't answer. Chatshell applies the message; products do nothing." A product never calls this itself: chatshell wires tea.RequestBackgroundColor()/BackgroundColorMsg handling in automatically (see chatshell.go's Init/Update). Passing nil clears it, reverting TerminalBackground() to the guessed default and leaving Dark as whatever it was already set to.

func StatusInset

func StatusInset() (left, right int)

StatusInset returns the LEFT/RIGHT column padding StatusLine (the hints/status bar) keeps — aligned to the composer's own SURFACE edge (the column immediately after its marker column, where its own ▄/▀ edge itself begins and ends), NOT the card/composer TEXT column HintsInset aligns the top bar to — founder, r14, verbatim: "Status panel should be aligned with composer border, not composer text." Right is 0: the status line's own right end reaches the composer surface's right edge exactly, the same as the ▄/▀ edge (which spans the full outer width minus only the marker column).

func StatusLine

func StatusLine(width int, content string) string

StatusLine renders ONE hints/status-bar row with NO background at all (the terminal's own default background shows through, SGR 49) and StatusInset()'s horizontal padding — founder, r12, verbatim, seeing the rendered result in Warp: "status line should have top margin and have no background ... Should have horizontal padding"; r14, verbatim, superseding the alignment specifically: "Status panel should be aligned with composer border, not composer text." content may already carry its own nested styling (RenderHints' per-hint key/label colouring) — StatusLine adds none of its own beyond the padding, so those spans' own colours are the only styling on the line; unlike Bar, there is no fill to reassert after a nested reset (paintOver exists to protect a BACKGROUND from an embedded reset — with none set here, there is nothing for a reset to cut off).

func Success

func Success(s string) string

Success returns s in the success colour (green).

func SurfaceColors

func SurfaceColors() (bg, fg color.Color)

SurfaceColors returns the neutral panel/grid surface background+ foreground pair — RoleBlock's own colours — for any component that needs a plain themed surface (blending into its enclosing card) WITHOUT drawing its own full Card frame, e.g. tui/grid's non-highlighted cells, tui/sidebar's unselected rows, or a side panel's frame background. No component should pick its own background literal for this; read it from here so a grid cell, a sidebar row and a card never disagree about what "the surface" looks like.

func TerminalBackground

func TerminalBackground() color.Color

TerminalBackground is "the bare terminal background a card/composer surface reads its tint from, and must stay distinguishable against": the terminal's own ACTUAL reported background (SetTerminalBackground) when known, otherwise this package's guessed default for the current Dark variant — a typical terminal emulator default (not the most extreme possible pure black/white, which would understate the r9 regression this guess exists to catch: founder 2026-09-25, "the assistant card fill is indistinguishable from the terminal background" in dark mode was reproducible against a common near-black default like most terminals ship with, not against pure black). Every SurfaceDeltaPairs() entry is checked against this, and colorsFor blends every card surface FROM it.

func TextColor

func TextColor() color.Color

TextColor is the colour of ordinary text on the neutral surface.

func TopBar

func TopBar(width int, title, context string, items []MenuItem) string

TopBar renders the shared top-bar chrome: a bold title, an optional context string (e.g. the current project/space), and menu items with the active one underlined — DataTug's original topBar look, now the default for every product. A product supplies only title/context/items; chatshell.WithTopBarProvider wires this in automatically.

func Warning

func Warning(s string) string

Warning returns s in the warning colour (yellow).

func YellowText

func YellowText(s string) string

YellowText returns s coloured yellow.

Types

type ANSIColor

type ANSIColor string

ANSIColor is an ANSI SGR escape sequence that switches the foreground colour.

const (
	ANSIRed    ANSIColor = "\x1b[91m"
	ANSIGreen  ANSIColor = "\x1b[32m"
	ANSIBlue   ANSIColor = "\x1b[94m"
	ANSIGray   ANSIColor = "\x1b[90m"
	ANSIYellow ANSIColor = "\x1b[33m"
)

ANSI foreground colour sequences.

type ChipDeltaPair

type ChipDeltaPair struct {
	Name         string
	ComposerBG   color.Color
	Surface      color.Color
	MinimumRatio float64
}

ChipDeltaPair names the attachment-chip fill and the minimum contrast ratio it must have against ComposerColors()' background — for the CURRENT Dark variant — the source of truth TestChipDistinctFromComposer checks, mirroring SurfaceDeltaPairs' own card-vs-terminal floor but for chip-vs-composer.

func ChipDeltaPairs

func ChipDeltaPairs() []ChipDeltaPair

ChipDeltaPairs returns the one chip-vs-composer delta pair (unfocused chip fill vs the unfocused composer background it sits on — a focused chip uses FocusSurfaceColors(), already the maximum-contrast accent pair and not a "blend toward invisible" risk the way the unfocused chip is).

type ContrastPair

type ContrastPair struct {
	Name         string
	FG, BG       color.Color
	MinimumRatio float64
}

ContrastPair names one (foreground, background) combination this package actually paints together, and the minimum WCAG 2.x contrast ratio (see Contrast) it must meet.

func ContrastPairs

func ContrastPairs() []ContrastPair

ContrastPairs returns every (foreground, background) combination this package paints together, for the CURRENT Dark variant (call SetDark first to get the other variant's set) — the enumerable source of truth TestContrastMeetsWCAG checks, so a future colour change that regresses readability fails a test instead of shipping.

type Hint

type Hint struct {
	Key   string
	Label string
}

Hint is one key/action pair shown in the shared hints/status bar, e.g. {Key: "Enter", Label: "send"}.

type MenuItem struct {
	Label  string
	Active bool
}

MenuItem is one top-bar menu entry/tab, e.g. DataTug's "Project: Foo [F3]" segment or a tab strip's tab.

type Role

type Role string

Role identifies which card/accent style to use. It intentionally duplicates transcript.Role's string values rather than importing that package: theme is a leaf package every other tui/ package (including transcript) depends on, never the reverse.

const (
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleSystem    Role = "system"
	RoleError     Role = "error"
	// RoleBlock is a rich transcript.Block entry (a grid, an HTTP response,
	// a join-candidate list, ...) sitting in the same card frame as a plain
	// message.
	RoleBlock Role = "block"
)

type SurfaceDeltaPair

type SurfaceDeltaPair struct {
	Name         string
	Surface      color.Color
	MinimumRatio float64
	MaximumRatio float64
}

SurfaceDeltaPair names one card/composer surface fill and the contrast-ratio range (see Contrast) it must sit within against TerminalBackground(). MaximumRatio of 0 means no upper bound (every card role: more tint than the floor is fine, there's no "too much" ceiling for a card the way there is for the composer).

func SurfaceDeltaPairs

func SurfaceDeltaPairs() []SurfaceDeltaPair

SurfaceDeltaPairs returns every card-role and composer surface fill this package paints directly on the terminal background, for the CURRENT Dark variant — the source of truth TestSurfaceDistinctFromTerminalBackground checks, so a card/composer tint that's crept too close to TerminalBackground() (unreadable as "a card" at all, independent of its text's own contrast) -- or, for the composer, too far from it (the "bright slab" regression) -- fails a test instead of shipping.

Jump to

Keyboard shortcuts

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