theme

package
v1.7.2 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package theme owns kb's TUI design tokens: the semantic palette of docs/design/tui-design-spec.md section 1, the cached style factory of section 6, and the layout metrics and glyphs of section 2.

Nothing outside this package names a hex. Nothing outside this package constructs a lipgloss style; a seam test enforces that rule across internal/tui.

Index

Constants

View Source
const BandRest = Raised

BandRest is the unfocused column header band tier. Spec section 1.1 puts the band one step above the card surface and names that tier Raised, so BandRest is a same-hex alias and not a collision: selection exists only in the focused column, whose band is a solid hue fill, so a Raised card and a BandRest band can never share a column.

View Source
const ColorProfile = colorprofile.TrueColor

Color profile pinning for goldens, spec section 6.4. The renderer downsamples per cell against the detected profile, so a golden cell grid depends on the profile the program ran under. Production pins nothing; tests pin explicitly.

ColorProfile is the profile for palette goldens. An ASCII-pinned golden of a design whose entire depth model is background color asserts nothing about the design, so every board and overlay color golden pins truecolor.

View Source
const GradSteps = 24

GradSteps is the length of every prebuilt ramp. Spec section 10.1.1: a ramp's interior colors are not palette slots, so no per-slot style exists for them and section 6.2 forbids blending one per frame. The ramp is built once at a fixed length and resampled by index instead. Twenty-four is roughly 2.7x the longest gradient-bearing run in the spec; a longer run repeats colors, which is correct rather than an error.

View Source
const LabelWheel = 5

LabelWheel is the size of the label pill wheel of spec section 1.6.

View Source
const StructureProfile = colorprofile.ASCII

StructureProfile is the profile for goldens that assert layout, truncation and drop order rather than color. They strip ANSI anyway and stay cheap.

Variables

View Source
var DefaultTiming = Timing{
	FPS:         20,
	PlainStride: 2,

	BirthDelay:      5,
	BirthSteps:      20,
	ScrambleSteps:   3,
	EllipsisStride:  8,
	SuffixAfter:     60,
	BrandBirthSteps: 12,
	CelebrateSteps:  12,

	DialogGraceQuiet:           425 * time.Millisecond,
	DialogGraceMax:             1500 * time.Millisecond,
	DialogGraceReopen:          500 * time.Millisecond,
	ScrollActiveLinger:         2000 * time.Millisecond,
	DoubleClickWindow:          400 * time.Millisecond,
	InputCoalesce:              16 * time.Millisecond,
	KeyboardNavigationInterval: 50 * time.Millisecond,
	KeyboardNavigationQuiet:    100 * time.Millisecond,
	NoticeTTL:                  5000 * time.Millisecond,

	PollInterval:  1000 * time.Millisecond,
	AutoShipDelay: 350 * time.Millisecond,
	SimilarDelay:  400 * time.Millisecond,

	GeometrySliceTarget: 8 * time.Millisecond,
	GeometrySliceLimit:  16 * time.Millisecond,
}

DefaultTiming is the table of spec section 10.3.1. crush is the donor for every value: each governs when a message is scheduled rather than how a frame is drawn, so the donor's cell-buffer architecture does not reach this set.

DialogGraceQuiet is deliberately longer than DoubleClickWindow, so the trailing half of a double-click that opened a destructive prompt lands inside the grace by construction. Neither may be reduced without the other.

View Source
var TimingCollapsed = Timing{}

TimingCollapsed is the test configuration: the struct zero value. Every one-shot fires immediately and no frame clock runs. It is not "fast" timing, it is no timing, which is the only kind a golden can assert against.

The rate/count split against the duration split is what makes this work: zero means "do not run" to a clock and "run now" to a one-shot, and conflating the two produces a test that spins the CPU.

Functions

func Downsample

func Downsample(content string, profile colorprofile.Profile) string

Downsample rewrites rendered content as the given profile would emit it. It is the string-level counterpart of PinProfile, for widget goldens that render directly instead of running a program. An unrecognized profile leaves the content untouched rather than dropping it.

func PinColor

func PinColor() tea.ProgramOption

PinColor pins a program to the truecolor profile of the reference design.

func PinProfile

func PinProfile(profile colorprofile.Profile) tea.ProgramOption

PinProfile returns the program option that pins a golden's color profile.

func PinStructure

func PinStructure() tea.ProgramOption

PinStructure pins a program to the colorless profile used by layout goldens.

func Tick

func Tick(d time.Duration, msg tea.Msg) tea.Cmd

Tick schedules msg after d. A non-positive d dispatches immediately instead of scheduling: tea.Tick(0, ...) still round-trips through the runtime timer, which is a wall-clock dependency wearing a zero. Every tea.Tick call site under internal/tui goes through here.

func WheelIndex

func WheelIndex(tag string) int

WheelIndex is the label color hash of spec section 1.6: the hash the board has always used, carried over unchanged so existing label colors stay stable relative to each other. It lives here beside LabelSlot because the per-board accent of section 10.7.2 derives from the same wheel, and the derivation must not fork the hash.

Types

type Band

type Band uint8

Band names one overlay band row, for the re-arming run of BandRun.

const (
	BandHeader Band = iota
	BandHeaderArmed
	BandSection
	BandFooter
)

The bands of spec section 4, plus the armed header of section 10.1.4.

type BoardStyles

type BoardStyles struct {
	Canvas  lipgloss.Style // page ground
	TopBar  lipgloss.Style // brand row
	Toolbar lipgloss.Style // filter and action row
	Footer  lipgloss.Style // status bar
	PagePad lipgloss.Style // the canvas row between the toolbar and the columns
}

BoardStyles are the page-level surfaces of spec section 2.1.

type ButtonSet

type ButtonSet [numButtonVariants]ButtonStyles

ButtonSet is the button token matrix: one ButtonStyles per variant, built once beside every other style.

func (ButtonSet) Variant

func (b ButtonSet) Variant(variant ButtonVariant) ButtonStyles

Variant returns the styles of one variant. An out-of-range variant resolves to Neutral rather than panicking a render path.

type ButtonStyles

type ButtonStyles struct {
	Rest    lipgloss.Style
	Focused lipgloss.Style
	Hovered lipgloss.Style
	Armed   lipgloss.Style
	Pressed lipgloss.Style
}

ButtonStyles are the states of one button variant. Armed is kb's addition for the purge and remove two-step; Pressed is the promoted reverse-video feedback that pointer.State.Render writes by hand today.

type ButtonVariant

type ButtonVariant uint8

ButtonVariant names what a button does, never what it looks like. Spec section 5.4 assigns one to every button surface in the TUI.

const (
	ButtonNeutral ButtonVariant = iota // dismissal, navigation, a side action
	ButtonPrimary                      // the pane's main affirmative
	ButtonSuccess                      // the state-advancing action
	ButtonDanger                       // the destructive action

)

The variants of spec section 1.9. Neutral is the zero value: a caller that states no meaning gets the calmest surface, not an accidental accent.

type CardStyles

type CardStyles struct {
	Rest     lipgloss.Style
	Zebra    lipgloss.Style
	Raised   lipgloss.Style
	Title    lipgloss.Style
	TitleSel lipgloss.Style
	Desc     lipgloss.Style
	Seq      lipgloss.Style
}

CardStyles are the card surfaces and text roles of spec section 3. Rest, Zebra and Raised are the three surfaces a card can sit on; the text roles are cached against Rest and recomposed with On for the other two.

type ChipStyles

type ChipStyles struct {
	Pad            lipgloss.Style
	ScopedPad      lipgloss.Style
	Body           lipgloss.Style
	BodyHover      lipgloss.Style
	BodyFocus      lipgloss.Style
	BodyFocusHover lipgloss.Style
	ScopedKey      lipgloss.Style
	Flat           lipgloss.Style
	FlatHover      lipgloss.Style
}

ChipStyles are the runs of one pill (spec section 3.6): a padding cell on the pill's own ground at each end, the text body on the fill, the dark half of a scoped pill, and the compact flat form.

Pad and ScopedPad are the padding runs that replaced the half-block end caps (issue #227). A cap drew the fill color as a *glyph* over the ground behind, so it read as a colored bar wherever the font did not draw a half block flush to the cell edge, and as a bar by construction on the inactive pill, which has no fill for it to fuse into. The padding cell is the same column spent as a colored cell instead of a glyph: Pad is the body's own ground, ScopedPad the key half's, so the pill is a flat span of color that owes the font nothing.

BodyHover and FlatHover are the hovered forms of spec section 10.5.1: an underline on the body run, zero cells, no color moved. BodyFocus is the keyboard-traversal cue that replaced the thickened caps - bold on the body run, likewise zero cells. Bold is available here for the same reason the underline of section 10.4.2 is: section 2.6 step 7 spends bold on the *compact flat* chip, which has no focus state and never shares a widget with a padded pill, so the two boldings can never be read in one place. BodyFocusHover is the composition, because focus and hover are orthogonal axes and the filter bar can hold both at once.

type ColumnStyles

type ColumnStyles struct {
	Panel     lipgloss.Style // panel body
	BandRest  lipgloss.Style // unfocused band, non-hued cells
	BandFocus lipgloss.Style // focused band on the brand hue
	BandLabel lipgloss.Style // band label on the unfocused surface
	Meta      lipgloss.Style // the "N cards - N blocked" row
	More      lipgloss.Style // the "+N more" overflow cue
}

ColumnStyles are the panel and header band surfaces of spec section 2.2. The hued halves of a band are composed with On and OnBold: BandRest carries the unfocused band's surface and BandFocus the focused band's foreground, so the four column hues do not each need their own cached style.

type Density

type Density uint8

Density is the layout density the frame resolved to. Spec section 2.6: compaction is not gradual, crossing the threshold applies all of it at once.

const (
	DensityNormal Density = iota
	DensityCompact
)

The two densities of spec section 2.6.

func (Density) Compact

func (d Density) Compact() bool

Compact reports whether the density drops the description snippet, the gutters and the pill end caps.

type Fidelity

type Fidelity uint8

Fidelity is the terminal floor of spec section 10.7.5, resolved once from the detected color profile so no view ever branches on the profile itself. Truecolor is the reference target and everything below it is a degradation, never a second design.

The word is deliberately not "tier": spec section 1.1 already spends that on the shade depth model.

const (
	FidelityFlat    Fidelity = iota // no color at all: glyph, weight and geometry only
	FidelityIndexed                 // flat palette slots, no blends
	FidelityFull                    // gradients, blends and color-carried effects run
)

The three floors of spec section 10.7.5.

func FidelityFor

func FidelityFor(profile colorprofile.Profile) Fidelity

FidelityFor resolves a detected profile to its floor.

colorprofile.Unknown maps to FidelityFull because it only ever occurs before detection completes - bubbletea always resolves a profile before it sends ColorProfileMsg - and the reference target is the correct assumption until the terminal says otherwise. This mirrors spec section 6.3's rule of defaulting isDark to true until the background message lands.

type Glyphs

type Glyphs struct {
	Rail     string // U+258C, resting card rail, unfocused band rail, meter fill
	RailFull string // U+2588, selected card rail, scrollbar thumb
	CapL     string // U+2590, meter left end cap (spec section 10.1.3; the pill lost its caps to issue #227)
	CapR     string // U+258C, meter right end cap
	Dot      string // U+25CF, column status dot
	Check    string // U+2610, unchecked checklist row
	CheckOn  string // U+2611, checked checklist row
	CheckOff string // U+2612, dropped checklist row
	Tick     string // U+2713, resolved-blocker mark inside a blocker chip
	Diamond  string // U+25C7, effort marker for a value outside the S/M/L scale
	Focus    string // U+25B8, focused band caret
	More     string // overflow cue prefix, rendered as "+N more"
	Ellipsis string // U+2026, the truncation tail of spec section 3.3
	// Blocked is the card's blocked alarm, drawn beside the sequence number of
	// spec section 3.2. It is the vocabulary's only pictograph, and it answers
	// to the emoji admission rule of section 10.4.1: a single code point with
	// Emoji_Presentation=Yes, no variation selector, no zero-width joiner and no
	// modifier, so the terminal has one rune to draw and the width every layout
	// calculation assumes is the width it takes. It is East Asian Wide and so
	// two cells, and it is bound by the adjacency rule - the render site writes
	// a space after it.
	//
	// It is admitted where the three effort squares were retired (issue #232)
	// because it carries a binary alarm rather than a value: a font with no
	// pictograph draws tofu beside the sequence number, and tofu beside a
	// sequence number still says this card is flagged. The squares carried one
	// of three values, and a partial or substituted glyph there lost which.
	Blocked string // U+26D4, the blocked alarm of spec section 3.2
	Track   string // U+2591, progress meter and scrollbar track (section 10.1.3)
	Empty   string // U+25CB, empty-state mark (section 10.8.3)
	Alert   string // U+25B2, failure mark (section 10.8.5)
	Bullet  string // U+00B7, meta separator and card description list marker
	HintSep string // hint ladder separator, three cells (section 10.4.6)

	// The half-block pair of spec section 10.6.1. These are the only glyphs in
	// the vocabulary that widen the block-glyph risk section 3.6 records rather
	// than inheriting it, and they are accepted for the launch mark alone: the
	// mark is decoration, and every fact the launch screen carries lives in the
	// meta row as plain text (section 10.6.1, glyph vocabulary cost).
	HalfTop    string // U+2580, brand letterform upper half
	HalfBottom string // U+2584, brand letterform lower half

	// Markers are the ASCII prefixes of spec section 10.4.1 that are display
	// vocabulary rather than prose. MarkSeq and MarkTag are a same-text alias,
	// deliberate and not a collision: they answer to different sections and
	// either may be re-spelled without the other.
	MarkSeq string // card reference prefix, "#142" (section 3.2)
	MarkTag string // plain label pill prefix, "#tag" (section 3.5)
	MarkDue string // compact due prefix, "!2d" (section 3.4)

	// The filter bar's toggle marks. A filter label pill is the section 3.6
	// pill plus one of these inside its caps, so the toggle survives the flat
	// fidelity floor of section 10.7.5, where the filled/tinted distinction that
	// carries the state everywhere else has no color to carry it with. Both are
	// two cells - the mark and the column that separates it from the pill body -
	// so the pill's width is the same in both states (section 10.4.4).
	MarkFilterOff string // unselected filter label pill, "+ #tag"
	MarkFilterOn  string // selected filter label pill, "x #tag"
}

Glyphs are the accent vocabulary of the design language. They are tokens and live beside the colors.

Known risk, carried consciously (map #136): the accent vocabulary is built from U+2588 / U+258C / U+2590. On fonts without block glyphs it degrades worse than a border would. Accepted at the issue #137 resolution. Spec section 10.4.1 makes this struct the only place under internal/tui where a display glyph or a separator may be written as a literal: views and widgets name tokens, and a view that needs a mark the vocabulary does not carry has found a missing token, not a reason for a literal.

type Metrics

type Metrics struct {
	WideFrame       int // frame width at or above which the page keeps a margin
	PageMarginX     int // left/right page margin on a wide frame
	PagePadTop      int // canvas row between the toolbar and the columns, normal only
	ColumnGutter    int // columns between panels
	ColumnPadX      int // inset of the card stack inside its panel, normal only
	ColumnMetaInset int // meta line and "+N more" inset from the panel edge
	CardGap         int // rows between stacked cards, normal only
	CardRail        int // reserved on the card's left edge, always
	CardPadLeft     int // between rail and content, normal only
	CardPadRight    int // always
	CardMinInner    int // below this a card renders surface and rail only
	MinColumnWidth  int // narrowest panel a column may shrink to and still hold a title
	BandHeadW       int // band prefix before its label, fixed across focus (section 10.4.4)
	ButtonPadX      int // left and right padding of one button (section 10.4.2)
	ButtonGap       int // surface-filled gap between two buttons in a row
	FocusGutterW    int // gutter column, reserved on every focusable non-card row
	FocusGutterGap  int // column between the gutter and the row's content
	MeterCells      int // default bar width of the progress meter (section 10.1.3)
	MeterMinCells   int // below this a meter renders its label only, no bar

	// The empty-state row of section 10.8.3. The two minimums are the rungs of
	// its width ladder: the headline is dropped before the action tail, because
	// the tail is the actionable half.
	EmptyHeadlineMin int // inner width at or above which an empty row keeps its headline
	EmptyActionMin   int // inner width at or above which an empty row keeps its action tail
	ActionGap        int // columns before an empty row's action tail

	// The busy and error rows of section 10.8.4 and 10.8.5.
	BusyGap       int // columns between a spinner frame and its label
	ErrorMaxLines int // lines an error message may wrap to inside a panel

	// The brand mark of spec section 10.6.8. The reveal's span is a timing
	// token and lives in Timing.BrandBirthSteps; the two half-block glyphs are
	// vocabulary and live in Glyphs.
	BrandMarkW       int // unstretched mark width, k(4) + BrandKern(1) + b(5)
	BrandMarkH       int // mark height, both letterforms
	BrandKern        int // blank columns between letterforms
	BrandStretchMax  int // inclusive upper bound of the memoized stretch
	BrandMetaW       int // meta row width before the frame cap
	BrandMetaGap     int // minimum columns between the meta row's two slots
	BrandMetaGapRows int // blank Canvas rows between the mark and the meta row
	BrandMinW        int // frame width below which the full mark is dropped
	BrandMinH        int // frame height below which it is dropped

	OverlayInsetX int // overlay content inset from the panel edge
	OverlayLabelW int // fixed label gutter of an overlay field row
	TableGutter   int // columns between two cells of a lipgloss table row
	CompactBelow  int // frame height below which density compacts
	CompactInnerW int // column inner width below which density compacts
	DescTwoLines  int // frame height at or above which the snippet gets a second line

	// The card row ceilings of spec section 2.6, rewritten by issue #232, given
	// interior vertical rhythm by issue #240 and re-cut into ceilings by issue
	// #243. Each is a function of density and frame height and bounds what its
	// section may spend; how much of it the section actually takes is a function
	// of the card's own content.
	CardTitleLines   int // title rows a card carries on a frame at or above DescTwoLines
	CardDescMax      int // description rows a card may carry at its tallest
	CardDescStep     int // frame rows that buy one more description line
	CardLabelRows    int // label rows a card carries on a frame tall enough for them
	CardInnerPadRows int // interior blank rows a card carries at its tallest
	CardInnerPadTwo  int // frame height at or above which the card carries both

	Overlay OverlayMetrics
}

Metrics are the gutter, padding and threshold tokens of spec section 2.5, plus the compaction thresholds of section 2.6 and the overlay width caps of section 4. Every number here is normative; a slice that needs a value not written here has found a spec gap.

func (Metrics) BrandBlockH

func (m Metrics) BrandBlockH() int

BrandBlockH is the launch block height of spec section 10.6.7: the mark, the blank rows under it, and the meta row.

func (Metrics) BrandFits

func (m Metrics) BrandFits(frameWidth, frameHeight int) bool

BrandFits reports whether a frame is large enough for the full mark. Below either floor the launch screen drops the mark and renders the meta row alone (spec section 10.6.7).

func (Metrics) BrandMetaWidth

func (m Metrics) BrandMetaWidth(frameWidth int) int

BrandMetaWidth is the meta row's width on a frame of this width: the section 10.6.5 token capped to the frame less a page margin either side. It never goes below zero, so a one-column frame renders an empty row rather than a negative allotment.

func (Metrics) CardGapRows

func (m Metrics) CardGapRows(density Density) int

CardGapRows is the number of rows between stacked cards.

func (Metrics) CardInner

func (m Metrics) CardInner(width int, density Density) int

CardInner is the content width of a card of this total width. Spec section 2.5: normal spends rail, left pad and right pad; compact drops the left pad.

func (Metrics) CardPad

func (m Metrics) CardPad(density Density) int

CardPad is the gap between a card's rail and its content.

func (Metrics) CardRows

func (m Metrics) CardRows(frameHeight int, density Density) int

CardRows is the tallest one card may be: every section of spec section 3.1 at its ceiling, in one place.

Until issue #243 this was the card's height outright, and columnStackHeight reserved a column from it before any card was rendered. Content-sized cards retired the reservation - a column measures the cards it is about to draw (widget.CardHeight) and packs against that - and what is left here is the bound: no card at this density and frame height can draw more rows than this, whatever its content, because every section is capped and the two interior separators are the only other rows on it.

func (Metrics) ColumnPad

func (m Metrics) ColumnPad(density Density) int

ColumnPad is the inset of the card stack inside its panel.

func (Metrics) DensityFor

func (m Metrics) DensityFor(frameHeight, columnInnerWidth int) Density

DensityFor resolves the frame against the compaction thresholds. Spec section 2.6: compaction fires when the frame is short or a column is narrow.

func (Metrics) DescLines

func (m Metrics) DescLines(frameHeight int, density Density) int

DescLines is the most description rows a card may carry. Spec section 3.3 as issue #232 rewrote it: none when compact, one on a short normal frame, and one more for every CardDescStep rows above DescTwoLines up to CardDescMax.

The ladder spends height the frame has rather than height it might have: a 45-row terminal buys the second line the original rule bought, and only a terminal tall enough to keep the same number of cards on screen buys the third, fourth and fifth.

Issue #243 made it a ceiling rather than an allotment. The frame decides how much description a card may show; the description decides how much of that it takes, and a card with none takes no rows at all instead of holding blank ones open under the title.

func (Metrics) InnerPadRows

func (m Metrics) InnerPadRows(frameHeight int, density Density) int

InnerPadRows is the number of blank interior rows a card carries: the vertical rhythm of spec section 3.1 as issue #240 added it. The rows carry the card's own fill rather than the panel's, so the card still reads as one slab and the blank rows highlight, stripe and take a click with every other row of it.

The count is a rung of its own. Compact carries none - compact exists to be dense (section 2.6). Below CardInnerPadTwo the card affords exactly one, and it goes between the shared title/description block and the meta row, which is the boundary that carries the grouping: title and description are one unit of prose, and everything under them is chips. At or above CardInnerPadTwo the frame has the surplus for the second, which goes between the meta row and the label rows - data above it, navigation below.

func (Metrics) LabelRows

func (m Metrics) LabelRows(frameHeight int, density Density) int

LabelRows is the most label rows a card may carry. Spec section 3.5 as issue #232 rewrote it: labels own rows of their own below the meta line and wrap onto the second when one row does not hold them. Compact reports zero, because step 5 of the section 2.6 drop order merges the labels onto the meta row instead of giving them one.

Issue #243 made it a ceiling too: a card whose pills fit one row draws one, and a card with no labels draws none and loses the separator above them.

func (Metrics) OverlayContent

func (m Metrics) OverlayContent(paneWidth int) int

OverlayContent is the readable measure inside a panel of this width: the panel grows with the frame, the prose column inside it does not grow past the point where a line stops being scannable.

func (Metrics) OverlayElevated

func (m Metrics) OverlayElevated(paneWidth, paneHeight int) bool

OverlayElevated reports whether a panel of this size renders as an elevated panel. Spec section 4: below the minimum the overlay falls back to the full frame, which is what keeps the frozen dismissal behaviors reachable on a terminal too small to center anything in.

func (Metrics) OverlayFocusContent

func (m Metrics) OverlayFocusContent(paneWidth int) int

OverlayFocusContent is the readable measure inside a panel for a row that can take focus. Spec section 10.4.3: the focus gutter and its gap are reserved in every state, so a focusable row's prose column is two narrower than a static row on the same panel rather than reflowing when focus arrives.

func (Metrics) OverlayPane

func (m Metrics) OverlayPane(frameWidth, frameHeight int) (paneWidth, paneHeight int)

OverlayPane is the panel geometry of spec section 4. It has two regimes, split at the same WideFrame threshold the board collapses on, which is how a responsive modal behaves: a narrow frame has no width to give away, so the panel takes all of it but the slack; a wide frame gets a proportional panel with real backdrop around it instead of a fixed cap stranded in dead canvas.

func (Metrics) PageMargin

func (m Metrics) PageMargin(frameWidth int) int

PageMargin is the left/right page margin for a frame of this width.

func (Metrics) PagePad

func (m Metrics) PagePad(density Density) int

PagePad is the canvas row between the toolbar and the columns.

func (Metrics) TitleRows

func (m Metrics) TitleRows(frameHeight int, density Density) int

TitleRows is the most title rows a card may carry - one, or two where the frame affords the wrap and the title needs it (issue #243). Spec section 3.2 as issue #232 rewrote it: the title wraps rather than being ellipsized to one line, and it is the last allotted row that carries the ellipsis when the title still does not fit. Compact keeps the single row it always had.

Amended by issue #240. The short normal rung - a frame below DescTwoLines - now keeps a single title row too, and spends the continuation row it gives up on the interior separator of InnerPadRows. That is step 3 of the section 2.6 drop order applied one rung early, and it is the cheapest row on the card to spend: the spec already ranks a wrapped title's second line below the description, the first row still carries the whole of what a scan reads, and the ellipsis says the rest exists. The trade is exact - one row out, one row in - so the shortest normal frame keeps the card count it had before the rhythm arrived.

type OverlayMetrics

type OverlayMetrics struct {
	WidthPct     int // percent of the frame width a content panel spans
	HeightPct    int // percent of the frame height a content panel spans
	FrameSlackW  int // columns a proportional panel always leaves free
	FrameSlackH  int // rows a proportional panel always leaves free
	NarrowSlackW int // columns a narrow-frame panel leaves free
	NarrowSlackH int // rows a narrow-frame panel leaves free
	MinPaneW     int // narrowest panel the proportional rule will produce
	MinPaneH     int // shortest panel the proportional rule will produce
	MinW         int // below this the overlay falls back to full frame
	MinH         int
	ContentMax   int // readable measure cap for prose inside a panel
	TaskAction   int
	Help         int
}

OverlayMetrics is the proportional panel geometry of spec section 4: every content overlay spans a percentage of the frame rather than a fixed cap, so a laptop-sized terminal gets a laptop-sized panel. The two content-sized dialogs (the task action confirm and the keyboard help) keep their own width caps because their height is their content and blowing them up would frame a handful of rows in a screenful of surface.

type OverlayStyles

type OverlayStyles struct {
	Surf            lipgloss.Style
	HeaderBand      lipgloss.Style
	HeaderBandArmed lipgloss.Style
	SectionBand     lipgloss.Style
	FooterBand      lipgloss.Style
	Shadow          lipgloss.Style
	FieldLabel      lipgloss.Style
	FieldValue      lipgloss.Style
}

OverlayStyles are the elevation surfaces of spec section 4.

HeaderBandArmed is the one state in the TUI that recolors a header band (spec section 10.1.4, ratified call 6): an armed two-step re-fills the band to StatusAlarm with FgBase bold, the same pair section 1.9 gives the armed button, so the frame and the button say the same thing in the same color. A destructive prompt that is merely pending does not reach it - it re-ramps the section label and leaves the frame alone.

type Palette

type Palette [numSlots]color.Color

Palette resolves every slot to a terminal color.

type Ramp

type Ramp uint8

Ramp names one prebuilt foreground gradient. Spec section 10.1: a gradient is chrome that says something about state, never decoration laid over content, and the budget is four surfaces and five named ramps. A fifth surface is a spec change, not a slice decision.

const (
	GradSection       Ramp = iota // overlay section-break label, resting
	GradSectionDanger             // ... destructive pending
	GradSectionArmed              // ... armed
	GradMeter                     // progress meter fill
	GradWork                      // branded engine label; launch mark

)

The five ramps of spec section 10.1.2.

type Slot

type Slot uint8

Slot names one semantic role in the palette. Roles, not hues: a slot says what a color is for, never what it looks like.

const (
	// Depth tiers (section 1.1). Depth is carried entirely by background
	// shade; there are no box-drawing borders on cards or columns.
	Shadow      Slot = iota // #05070a x256 232 overlay drop shadow
	Canvas                  // #0b0e14 x256 233 page ground
	Surface                 // #171d27 x256 234 column panel body, footer bar, filter field
	Zebra                   // #1e2632 x256 235 alternating card tier, compact density only
	Card                    // #252f3d x256 236 card surface
	Raised                  // #35404f x256 238 selected card surface and the unfocused band
	OverlaySurf             // #3c495c x256 239 overlay panel body
	OverlayBand             // #4a5970 x256  59 overlay header/footer/section bands

	// Foreground scale (section 1.2).
	FgBase       // #e3e9f2 x256 255 primary text
	FgSubtle     // #9aa5b6 x256 248 secondary text
	FgMuted      // #6b7686 x256 243 tertiary text
	FgOnAccent   // #0b0e14 x256 233 text on a saturated fill
	Brand        // #4f8ef7 x256  69 wordmark pill, overlay header band, focus accent
	HueTodo      // #7aa2f7 x256 111 TO DO column identity
	HueDoing     // #f2a33c x256 215 DOING column identity
	HueDone      // #3fbf7f x256  72 DONE column identity
	HueCancelled // #7b8494 x256 102 CANCELLED column, hidden-count chip

	// Priority scale (section 1.4).
	Prio1 // #ff5a48 x256 203
	Prio2 // #ffb020 x256 214
	Prio3 // #4f8ef7 x256  69 low, and the fallback for any unknown priority

	// Status colors (section 1.5).
	StatusOK     // #3fbf7f x256  72
	StatusWarn   // #ffb020 x256 214
	StatusDanger // #ff5a48 x256 203
	StatusInfo   // #4f8ef7 x256  69
	StatusAlarm  // #b31f14 x256 124 armed two-step fill

	// Button variant tints (section 1.9): the readable-on-Raised form of a
	// variant's hue, and the fill its hovered state carries.
	TintPrimary // #a8b6ff x256 147
	TintSuccess // #7fe0b0 x256 115
	TintDanger  // #ffa7a0 x256 217

	// Label pill wheel (section 1.6), selected by the labelColor hash.
	Label1 // #ff7b54 x256 209
	Label2 // #4f8ef7 x256  69
	Label3 // #3f9d58 x256  71
	Label4 // #b98af7 x256 141
	Label5 // #ffb020 x256 214

)

The palette slots of spec section 1. Order is depth tiers, foreground scale, brand and column hues, priority scale, status colors, label wheel.

func AccentSlot

func AccentSlot(title string) Slot

AccentSlot is the per-project accent hue of spec section 10.7.2: a deterministic recognition cue for the board the user named, and nothing else. No state, count, severity or affordance may ever be encoded in it.

The accent derives from the section 1.6 label wheel rather than from free HSL, so it is total over {Brand, Label1..Label5} and can emit no hex the section 1.7 audit has not already cleared. The default and empty titles resolve to Brand: a board that declared no name looks exactly like kb looks without one.

func EffortSlot

func EffortSlot(value string) (Slot, bool)

EffortSlot resolves the fill hue of the section 3.4 effort pill for one effort value: the scale is S, M and L, and the ramp runs cool to warm.

Issue #232 retired the three colored squares the chip wore between #223 and #230. A pictograph proved font-dependent on real terminals however carefully its width was pinned, and the value it carried was one of three rather than a yes or no, so a substituted or clipped glyph lost information. The letter now sits on the fill instead of beside a square: ASCII on background color, which is the one thing section 3.6 says a pill owes a terminal font.

The three hues are palette slots that already exist and are already audited against FgOnAccent as pill fills. No new hex enters section 1.7's table for a three-value scale, and the letter - not the hue - is what carries the value.

An effort value the scale does not name resolves false, and the render site falls back to the Diamond mark it wore before the squares.

func LabelSlot

func LabelSlot(index int) Slot

LabelSlot maps a label wheel position onto its palette slot. Spec section 1.6: five colors, selected by the label hash the board already uses.

func PrioritySlot

func PrioritySlot(priority int) Slot

PrioritySlot maps a task priority onto its palette slot. Spec section 1.4: the scale is three values - high, medium, low - so exact match on 1 and 2, and everything else is low.

Issue #234 migrated the store onto the same three values, so the fourth hue is gone from the palette and the four-value data this seam was written to tolerate no longer reaches it: migrate runs inside store.Open before any caller can read a task, and the write paths reject a value off the scale. The default arm stays because Slot must be total - a zero-valued struct that never reached the store still has to render something, and low is what an unset priority means everywhere else.

func RampStops

func RampStops(ramp Ramp) (lead, tail Slot)

RampStops returns the lead and tail slot of a ramp, so a caller that has to pair a flat run with a graded one - the meter's end caps of section 10.1.3 - names the ramp rather than restating its endpoints. An out-of-range ramp resolves to GradSection rather than panicking a render path.

type StatusStyles

type StatusStyles struct {
	OK     lipgloss.Style
	Warn   lipgloss.Style
	Danger lipgloss.Style
	Info   lipgloss.Style
	Dot    lipgloss.Style
}

StatusStyles are the semantic status roles of spec section 1.5.

type Styles

type Styles struct {
	Pal    Palette
	Dimmed *Styles // nil on the dimmed instance itself; spec section 4 step 1

	Board   BoardStyles
	Column  ColumnStyles
	Card    CardStyles
	Rail    [4]lipgloss.Style // resting card rail by priority 1..3; index 0 unused
	RailSel [4]lipgloss.Style // the same rail on a selected card's Raised surface
	Chip    ChipStyles
	Label   [5]ChipStyles // the section 1.6 wheel
	Status  StatusStyles
	Overlay OverlayStyles
	Button  ButtonSet
	Pressed lipgloss.Style

	Table    TableStyles
	Input    textinput.Styles
	Area     textarea.Styles
	Help     help.Styles
	Spinner  spinner.Spinner
	Progress progress.Model
	Markdown glamour.StyleConfig
	Huh      *huh.Styles

	Work WorkStyles

	Metrics  Metrics
	Glyph    Glyphs
	Timing   Timing
	Fidelity Fidelity
	// contains filtered or unexported fields
}

Styles is the whole design system, resolved once. Spec section 6.2: every lipgloss style in here is constructed inside New, exactly once. New is called on program start and on tea.BackgroundColorMsg, nowhere else. The result is threaded down from the root model; there is no package-level mutable style state and no use of Style.Inherit, which silently skips padding and margins.

func New

func New(isDark bool) *Styles

New resolves the palette for a terminal background and builds every style exactly once. Both the base and the dimmed variant of spec section 1.8 are built here, so the overlay backdrop never blends a color per frame.

isDark arrives from tea.BackgroundColorMsg.IsDark(). Default to true until the message lands, then rebuild once.

func NewFor

func NewFor(isDark bool, profile colorprofile.Profile) *Styles

NewFor is New with the terminal floor of spec section 10.7.5 resolved from a detected color profile. New is NewFor at the truecolor reference target, so the huh ThemeFunc seam of spec section 6.3 keeps its exact signature.

func NewWith

func NewWith(isDark bool, timing Timing) *Styles

NewWith is New with the timing set of spec section 10.3 injected. Injection follows the cached factory of spec section 6.2 rather than mutating a built *Styles: the same Timing lands on the base instance and on Dimmed, so an overlay schedules against the same clock as the board behind it.

Production calls New. Tests that must collapse timing call NewWith with TimingCollapsed, which is the only configuration a golden can assert against.

func (*Styles) BandRun

func (s *Styles) BandRun(band Band, content string) string

BandRun lays an already-composed run into an overlay band row. An overlay band is rendered as one styled run over the whole row, so a colored fragment inside it - the branded spinner frame of spec section 10.2.5, which is the one part of a busy row that is supposed to carry a color - would close with a reset and drop the band's own foreground and background for every cell after it. Each reset in the run is followed by the band's own sequence instead.

Content with no reset in it is returned untouched, so a band row of plain text renders exactly the bytes it always did.

func (*Styles) ChipRuns

func (s *Styles) ChipRuns(fill, surface Slot) ChipStyles

ChipRuns returns the runs of one pill fill over one surface. Styles.Chip and Styles.Label are this composition against the resting card surface; a chip on any other surface resolves here, which costs struct copies and never a style construction.

The pill has no end caps since issue #227. Each end spends its column on a padding cell filled with the ground that run already owns - Pad is the body's fill, ScopedPad the Surface tier the scoped key half sits on - so the whole pill is a run of colored cells and nothing in it depends on a font drawing a half block flush to a cell edge. Width is unchanged: the space costs the cell the cap used to.

func (*Styles) ChipRunsTint

func (s *Styles) ChipRunsTint(fill, surface Slot) ChipStyles

ChipRunsTint returns the runs of one inactive pill over one surface: the same section 3.6 anatomy with the wheel hue withdrawn from the fill but kept on the text. The fill drops to Surface - the tier the scoped pill's dark half already uses - so the pill cannot be misread as selected, and the body run takes the fill hue as its foreground instead of FgOnAccent, which is the section 1.9 blurred-button pattern: a variant that has lost its state still carries its identity as a tint on the resting surface. An unselected filter label therefore reads as an offer, and still matches by eye the wheel-hued label pill on the card it filters for (issue #208).

The tint form keeps the two-tone split of the scoped pill: the key run stays one step below the body - FgSubtle under the hue - so the key/value hierarchy survives the withdrawal of the fill rather than collapsing into one run. That run also carries the toggle marker (section 10.4.1), which is why it sits at the secondary role rather than the tertiary one: the marker is the affordance a user must read before acting, so it takes the section 1.9 AA floor with it.

Both padding cells are the withdrawn Surface ground the tinted body and key runs already sit on (issue #227), so an inactive pill is one flat Surface span carrying hued text. The end caps this replaced were the worst case of the cap problem: an inactive pill has no fill for a hued half-block to fuse into, so the caps rendered as two colored bars flanking the word by construction rather than only on an unlucky font. The toggle affordance was never theirs - it is hue-on-fill against hue-on-surface plus the equal-width marker.

The compact run keeps the hue it already had: the flat form has no fill and no padding to spend, so there the marker is the whole toggle affordance, the same rule section 10.7.5 applies at FidelityFlat. No cell count changes in any form, which is what lets the filter bar toggle a pill in place (section 10.4.4).

func (*Styles) Fg

func (s *Styles) Fg(foreground Slot) lipgloss.Style

Fg returns the cached blank style carrying a foreground slot only, for runs that inherit the surface they are composed onto.

func (*Styles) Grad

func (s *Styles) Grad(ramp Ramp, text string) string

Grad paints text cluster by cluster along the named ramp, foreground only. Spec section 10.1.1: the run is split into grapheme clusters and each cluster is styled whole, because rune splitting recolors the inside of an emoji ZWJ sequence and puts an SGR change between a base character and its combining mark, which some terminals then draw as two cells.

The caller wraps the result in SurfaceRun: per-cluster output is the worst case of the hazard SurfaceRun exists for, and a gradient run that is not wrapped in it is a bug (spec section 10.1.1).

func (*Styles) GradBold

func (s *Styles) GradBold(ramp Ramp, text string) string

GradBold is Grad with the bold attribute already set.

func (*Styles) GradCell

func (s *Styles) GradCell(ramp Ramp, column, width int, cluster string) string

GradCell renders one already-split cluster at the ramp index column takes in a run of width columns. Spec section 10.2.5: the branded engine rebuilds one cell of one frame at a time rather than a whole run, so it needs the ramp resampled by position instead of Grad's whole-run walk.

The indexing is the section's own: column * (GradSteps-1) / max(width-1, 1), which shares a ramp style across neighbouring columns on a run longer than the ramp rather than blending a new one per cell.

func (*Styles) Graded

func (s *Styles) Graded() bool

Graded reports whether color-carried effects run on this terminal.

Spec section 10.7.5: this may be read for exactly one purpose, deciding whether to *start* an effect - arm a tick chain, allocate a prerender cache. It may never be read to pick a color. A view that writes "if styles.Graded() { colorA } else { colorB }" has reintroduced the bespoke 256-color design the section forbids.

func (*Styles) HoverRun

func (s *Styles) HoverRun(surface, raised Slot, content string) string

HoverRun raises one inline run a depth tier inside an already-styled row. Spec section 10.5.1 gives hover a one-tier raise in the section 1.1 depth order; an inline reference is not a row, so the raise spans the run's own cells and the row keeps its surface on both sides. The foreground is left alone - a rendered link keeps the section 5.2 link color - and no cell is added or removed, which is section 10.4.4 applied to the one state driven by cell coordinates.

A composed run may close with a reset that would drop the raise for the rest of the run, so the tier is re-armed after each one the way PressedRun does. The run closes by restoring the row's own surface rather than by resetting, because it is substituted into the middle of a styled line.

func (*Styles) HuhTheme

func (s *Styles) HuhTheme() huh.Theme

HuhTheme hands the already-built huh styles to a huh field. Spec section 6.3 registers the factory itself as huh.ThemeFunc; a field rendered from a *Styles that New has already resolved must not rebuild the palette per frame, so the func closes over the built styles and ignores the background argument it was resolved for.

func (*Styles) On

func (s *Styles) On(foreground, background Slot) lipgloss.Style

On returns the cached blank style carrying a foreground and background slot. The widget API of spec section 5.1 is slot-parameterized (chip fills, column hues), so the surface a run lands on is only known at render time; resolving it here keeps lipgloss.NewStyle out of every render path while still costing nothing but a struct copy.

func (*Styles) OnBold

func (s *Styles) OnBold(foreground, background Slot) lipgloss.Style

OnBold is On with the bold attribute already set.

func (*Styles) OnItalic

func (s *Styles) OnItalic(foreground, background Slot) lipgloss.Style

OnItalic is On with the italic attribute already set, for the emphasis run of the card description's markdown (spec section 3.3, issue #232). Italic is an attribute and costs no cell, so a description that carries emphasis wraps to the same rows as one that does not.

func (*Styles) PressedRun

func (s *Styles) PressedRun(content string) string

PressedRun wraps an already-composed run in the Pressed token. Spec section 9.1: the reverse-video feedback is a theme token, not a raw escape written by the pointer package.

A themed control renders its own styles inside this run, and every one of them ends in a reset that would cancel the feedback for the rest of the row, so the attribute is re-armed after each reset. The run closes by clearing the attribute alone rather than resetting the whole style, because the caller may be substituting it into the middle of a styled line.

func (*Styles) RowSurface

func (s *Styles) RowSurface(hovered bool) Slot

RowSurface returns the surface for one overlay choice row. Spec section 10.5.1: hover raises the whole row one tier in the section 1.1 depth order, panel edge to panel edge, and OverlayBand under FgBase is the pair section 1.9 already measured as the Neutral hovered button, so a hovered row and a hovered Neutral button in the same panel read as one system.

The row does not bold: bolding a full-width run is a shout where bolding a six-cell label is a nudge. A row's own selected state is the focus gutter glyph of section 10.4.3, which does not spend the tier step, so the full-row raise collides with nothing.

func (*Styles) Surface

func (s *Styles) Surface(selected, alternate bool) Slot

Surface returns the card surface for a card in this state. Spec section 2.1: selection steps Card to Raised, and the alternating tier is compact-only.

func (*Styles) SurfaceRun

func (s *Styles) SurfaceRun(surface Slot, content string) string

SurfaceRun lays an already-composed run onto a surface slot. An adopted charm component paints its own runs and closes each one with a reset, which drops the panel surface for every cell the component itself did not paint - the single space bubbles/help writes between its key and description columns is the case this exists for. The background is armed once at the front and re-armed after every reset, so a component's output carries the surface edge to edge without kb reformatting what the component rendered.

The run closes with a full reset because it is laid down as a whole row, not substituted into the middle of one the way PressedRun is.

type TableStyles

type TableStyles struct {
	Cell lipgloss.Style // every column but the last: carries the gutter
	Last lipgloss.Style // the last column: no trailing gutter to spend
}

TableStyles are the cell styles of an adopted lipgloss/v2 table. They carry layout only - the column gutter of spec section 2.5 and nothing else - so a table lays out plain text that the view then paints with the token its row role names. A cell style that carried a color would fight the surface the row is composed onto.

func (TableStyles) Column

func (t TableStyles) Column(index, count int) lipgloss.Style

Column returns the cell style for column index of a row that has count columns.

type Timing

type Timing struct {
	FPS         int
	PlainStride int

	BirthDelay      int
	BirthSteps      int
	ScrambleSteps   int
	EllipsisStride  int
	SuffixAfter     int
	BrandBirthSteps int
	// CelebrateSteps is the span of the ship celebration (issue #191). Spec
	// section 10.3.1 names no such token, so this is the twentieth and the one
	// the section's own rule sends back here rather than to a call site. It is
	// a tick count for the same reason BrandBirthSteps is: both index a
	// class-B effect that collapses to nothing at zero.
	CelebrateSteps int

	DialogGraceQuiet           time.Duration
	DialogGraceMax             time.Duration
	DialogGraceReopen          time.Duration
	ScrollActiveLinger         time.Duration
	DoubleClickWindow          time.Duration
	InputCoalesce              time.Duration
	KeyboardNavigationInterval time.Duration
	KeyboardNavigationQuiet    time.Duration
	NoticeTTL                  time.Duration

	PollInterval  time.Duration
	AutoShipDelay time.Duration
	SimilarDelay  time.Duration

	// GeometrySliceTarget is the preferred amount of offscreen layout work per
	// worker command. GeometrySliceLimit is the hard ceiling used to report an
	// overrun. Both are worker budgets, not paint-path timing.
	GeometrySliceTarget time.Duration
	GeometrySliceLimit  time.Duration
}

Timing is the clock of spec section 10. Every duration the TUI schedules against is named here; nothing under internal/tui writes one inline. Rates and tick counts are int; one-shots are time.Duration.

Spec section 10.3.1 is the normative table and this struct is its Go shape. Two hazards bound the whole family and both are enforced rather than documented: a duration may never be read on a paint path, and every effect must collapse to zero. TimingCollapsed is the second one; theme/seam_test.go is the first.

func (Timing) CelebrateBeat

func (t Timing) CelebrateBeat() int

CelebrateBeat is the tick length of one phase of the ship celebration, or 0 when the effect is collapsed. The beat is derived rather than named so the span stays the one reviewable number: shortening the flourish shortens every phase of it by construction.

func (Timing) Interval

func (t Timing) Interval() time.Duration

Interval is the tick period of the one clock, or 0 when motion is off.

func (Timing) PlainFrame

func (t Timing) PlainFrame() time.Duration

PlainFrame is the tick period of a plain-tier spinner.

type WorkStyles

type WorkStyles struct {
	Label  lipgloss.Style // plain-tier spinner and static busy text
	Birth  lipgloss.Style // the branded engine's pre-birth cells
	Suffix lipgloss.Style // the elapsed-time field right of the label
}

WorkStyles are the busy-state text roles of spec section 10.2. They are foreground only: the branded engine composes a run of them and the caller lays that run onto its own shade tier with SurfaceRun, so a style that carried a background would fight the surface behind the row.

Jump to

Keyboard shortcuts

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