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
- Variables
- func AccentColor() color.Color
- func Bar(width int, content string) string
- func BlueText(s string) string
- func BorderColor(focused bool) color.Color
- func Card(role Role, header, body string, width int, focused bool) string
- func ChipCloseColor(bg color.Color) color.Color
- func ChipColors() (bg, fg color.Color)
- func Colorize(s string, color ANSIColor) string
- func ComposerChipLeadingFill() int
- func ComposerChipMarker(focused bool) string
- func ComposerColors() (bg, fg color.Color)
- func ComposerFocusColors() (bg, fg color.Color)
- func ComposerFrame(width int, content string, focused bool) string
- func ComposerFrameNoTopEdge(width int, content string, focused bool) string
- func ComposerFrameSize() (cols, rows int)
- func ComposerTextColumn() int
- func ContentMargins(terminalRows int) int
- func Contrast(a, b color.Color) float64
- func ContrastText(bg color.Color) color.Color
- func Danger(s string) string
- func Detect() (isDark bool)
- func ErrorColor() color.Color
- func FocusColor() color.Color
- func FocusSurfaceColors() (bg, fg color.Color)
- func GrayText(s string) string
- func GreenText(s string) string
- func HalfBlockEdge(width int, surfaceBG color.Color, top bool) string
- func HalfBlockEdgesActive() bool
- func HeaderFor(role Role) string
- func Hex(c color.Color) string
- func HintsInset() (left, right int)
- func InnerWidth(width int) int
- func MutedColor() color.Color
- func PaintOver(content string, bg, fg color.Color) string
- func PanelColors() (bg, fg color.Color)
- func PanelFocusColors() (bg, fg color.Color)
- func PanelFrame(width, height int, header, content string, focused bool) string
- func PanelFrameSize() (cols, rows int)
- func PanelHeader(title string) string
- func RedText(s string) string
- func RenderHints(width int, hints []Hint, segments ...string) string
- func ReserveMarkerColumn(content string) string
- func SelectedRow(text string, selected bool, width int) string
- func SelectedStyle(focused bool) lipgloss.Style
- func SetColorProfileDetector(detect func() colorprofile.Profile) (restore func())
- func SetDark(dark bool)
- func SetDefaultColor(color ANSIColor)
- func SetHalfBlockEdges(v bool)
- func SetTerminalBackground(c color.Color)
- func StatusInset() (left, right int)
- func StatusLine(width int, content string) string
- func Success(s string) string
- func SurfaceColors() (bg, fg color.Color)
- func TerminalBackground() color.Color
- func TextColor() color.Color
- func TopBar(width int, title, context string, items []MenuItem) string
- func Warning(s string) string
- func YellowText(s string) string
- type ANSIColor
- type ChipDeltaPair
- type ContrastPair
- type Hint
- type MenuItem
- type Role
- type SurfaceDeltaPair
Constants ¶
const CardPaddingCols = 2
CardPaddingCols/Rows is the space, in columns/rows, kept between a card's fill edge and its content.
const CardPaddingRows = 1
const MarginCollapseRows = 24
MarginCollapseRows is the terminal row count AT OR BELOW which ContentMargins returns 0 — founder: "collapse both below 24 terminal rows".
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.
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.
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 ¶
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.
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.
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).
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 ¶
AccentColor is the shared emphasis colour for a hint's key/shortcut (distinct from FocusColor, which marks focus/selection specifically).
func Bar ¶
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 BorderColor ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
ErrorColor is the colour of error text: a red that reads on both variants.
func FocusColor ¶
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 ¶
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 HalfBlockEdge ¶
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 ¶
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 ¶
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 ¶
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 ¶
MutedColor is the shared low-emphasis text colour (hint labels, an empty sidebar's placeholder, unfocused chrome borders).
func PaintOver ¶
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 ¶
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 ¶
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 ¶
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 ¶
PanelHeader renders a side-panel/sidebar title header, e.g. "Sidebar" or a product workspace pane's tab name.
func RenderHints ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 SurfaceColors ¶
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 ¶
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 TopBar ¶
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.
Types ¶
type ANSIColor ¶
type ANSIColor string
ANSIColor is an ANSI SGR escape sequence that switches the foreground colour.
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 ¶
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 ¶
Hint is one key/action pair shown in the shared hints/status bar, e.g. {Key: "Enter", Label: "send"}.
type MenuItem ¶
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.