tokens

package
v0.3.1-rc.1 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package tokens is the shared token layer for the chat surfaces: the one place a colour, a glyph, or a formatted live cell is named.

It is a leaf over the standard library. Nothing in this package imports a surface, so every surface above may depend on it and none of it may depend on a surface.

Everything here is pure data or a pure function. There is no init function, no mutable global, and no I/O. The colour table is built once, at package initialization, by one function reading one file's literals — so a render never formats an escape sequence, it appends a constant string.

Why a token layer exists (the August 2026 chat-rebuild audit, no longer in the tree, 10.1.1 and 5.16)

Today's chat renders colour from ad-hoc lipgloss values scattered through the render code, so the palette cannot be tested, cannot be degraded for a 256-colour terminal in one place, and cannot be proven legible. Here a token is a named thing with a base value, a dimmed value, a contrast class, and a resolution per terminal capability — and the contrast law is a unit test (contrast_test.go) rather than a paragraph of intent.

The axes

Three axes compose, and they are orthogonal:

  • HUE — what a thing means. Five words and no more (5.16): amber = needs a human, cyan = alive, green = money and success, coral = broken, and a per-task identity pastel from an 8-hue wheel. Everything else is the three-tier grey ramp (5.13). See Hue.
  • STATE — how live a thing is (8.1.6): accent = live, plain = settled, dim = chrome. See State and ResolveToken, which is the whole composition rule in one function.
  • FOCUS — whether the pane owning the row has the user's attention. Every token carries a base value and a dimmed value (Focus); dimming is a property of the pane, not of the row.

The contrast law (the shipping gate)

5.16 requires every pastel to pass contrast on the default dark ground AND on the dimmed variant. That is enforced in code: Pairings enumerates every legal (foreground, ground) combination the surface may draw, and contrast_test.go walks all of them computing WCAG relative luminance. A pairing that is not legal is not merely untested — it is a combination the surface is forbidden to draw, and the reason is stated at Legal.

What the surface packages consume

tokens.NewStyler(profile, tokens.FocusNormal)
tokens.Amber.Fg(profile, tokens.FocusNormal)   // an SGR string, precomputed
tokens.Amber.Hex(tokens.FocusNormal)           // "#EECE96", for lipgloss
tokens.ResolveToken(tokens.HueBroken, tokens.StateSettled)
tokens.GlyphWaitsOn, tokens.Gauge(0.62), tokens.GlyphCut
tokens.AppendElapsedCell(buf[:0], d)           // zero allocations
sanitize.TextWithPalette(s, sanitize.Table(tokens.ANSI16Remap))

Section numbers in comments refer to the August 2026 chat-rebuild audit, no longer in the tree.

Index

Constants

View Source
const (
	CodeTextHex     = "#E6E6F0" // the primary text tier, restated so the ramp reads whole
	CodeKeywordHex  = "#C9BDE5" // indigo
	CodeStringHex   = "#ADE0BE" // leaf
	CodeNumberHex   = "#E8CFA4" // sand
	CodeCommentHex  = "#838A9E" // slate — the one receding slot
	CodeFunctionHex = "#A8D6EA" // sky
	CodeTypeHex     = "#E8B4CE" // orchid
)

The ramp, as literal hex, in slot order. These are the only hand-authored colours this file adds, and they are pitched one step off the semantic hues on purpose: close enough to belong to the same palette, far enough that a keyword is never mistaken for a state.

View Source
const (
	// GlyphProseBullet is the mark on an unordered list item. It is the middle
	// dot rather than U+2022 because the middle dot is already measured and
	// already shipping in this tree, and because 5.13 asks structure to be the
	// quietest thing on the row: the INDENT is what says "list", the mark only
	// says "item".
	GlyphProseBullet = "·"
	// GlyphProseQuote is the gutter bar down the left of a blockquote —
	// structure without boxes (5.21), one column wide.
	//
	// IT IS THE HAIRLINE AND NO LONGER THE BOX RULE. It was `│` — the same byte
	// the spawn tree's trunk ([GlyphTreeVert]) and every vertical rule in the
	// product are drawn from — and a blockquote sat at the left margin of the
	// chat feed while the margin column's own divider ran down the right of the
	// very same rows, so on a screen with no other vertical rules two `│`
	// columns meaning unrelated things read as one broken frame. The fence
	// beside it already owned the right mark for "this block is set apart", so
	// the quote takes [GlyphCodeGutter]'s byte instead: an eighth of a cell,
	// which is a margin and cannot be mistaken for a frame.
	GlyphProseQuote = "▏"
	// GlyphCodeGutter is the hairline down the left of a fenced code block. It
	// is one eighth of a cell of ink: enough to bound the block on the side the
	// eye returns to, and far too little to read as a border. There is no line
	// number beside it — a number column is a second thing to read in a region
	// the reader opened in order to read something else.
	GlyphCodeGutter = "▏"
)

Prose glyphs (5.17). Both are bytes the vocabulary already ships elsewhere, named again here because a slot is a MEANING and not a byte: a renderer that reached for GlyphSeparator to draw a list bullet would be one rename away from drawing telemetry dots down the left margin. glyph_test.go pins each of these equal to the glyph it shares a byte with, so the duplication can never become a drift.

View Source
const (
	// CountCellWidth fits "999", "1.5K", "25K", "1.2M" — the ladder never
	// produces more than four cells.
	CountCellWidth = 4
	// DurationCellWidth fits "4.5s", "45s", "3m12s", "59m59s", "2h14m",
	// "99d23h".
	DurationCellWidth = 6
	// ElapsedCellWidth fits "45s", "5m", "1h02", "99d23" — the aging form,
	// which drops the trailing unit so the granularity switch never jitters.
	ElapsedCellWidth = 5
	// PercentCellWidth fits "5.1%" and "100%".
	PercentCellWidth = 4
	// ContextCellWidth fits "5.1%/1M" and "100%/128K".
	ContextCellWidth = 9
	// MoneyCellWidth fits "$999.99" and "$1.2K".
	MoneyCellWidth = 7
)

Cell widths. These are display cells, not bytes; every numeric form is ASCII, and GlyphMissing is the one multi-byte single-cell exception.

View Source
const (
	// State vocabulary. Shape encodes state CATEGORY and may change at a true
	// state transition; shape never animates on a long-lived row (8.1.6).
	GlyphQueued  = "○"
	GlyphWorking = "◐"
	GlyphSettled = "✓"
	GlyphFailed  = "✕"
	// GlyphStopped is work A PERSON ENDED, and it is deliberately neither
	// [GlyphSettled] nor [GlyphFailed]: a tick is a finding that the work came
	// off and a cross is a finding that it did not, and nobody found anything
	// about work somebody stopped. The filled square is the mark every device a
	// person owns stops with, it is one cell under both shipping rulers, and it
	// is the shape the nerd-font tier has an exact icon for (nf-fa-stop).
	GlyphStopped = "■"
	// GlyphPaused is 5.17's replacement for the banned ⏸: a paused row is
	// "=" (or a dim GlyphQueued, at the renderer's choice).
	GlyphPaused = "="

	// Attention and blocking.
	GlyphNeedsHuman = "?" // always amber (5.16)
	GlyphWaitsOn    = "⚑" // waiting on a sibling (waits-on edge)
	// GlyphWithdrawn is a question that STOPPED BEING A QUESTION — its subject
	// went away, the plan changed, or another answer made it moot, so the asker
	// took it back (docs/design/questions/DESIGN.md's WITHDRAWN, WITH A REASON).
	//
	// It is deliberately none of the three marks it sits nearest. A tick says
	// somebody decided; a cross says the answer was no; a filled square says a
	// person ended it. Nobody decided anything here and nobody ended anything —
	// the decision simply stopped needing to be made — and the circled slash is
	// the one shape in the geometric register that says "this does not apply"
	// without claiming an outcome.
	GlyphWithdrawn = "⊘"
	// GlyphAssumed is the ladder's SECOND rung drawn: the asker has taken
	// something for granted, said so, and gone on — and everything on the card
	// stands until somebody strikes it (docs/design/questions/DESIGN.md names
	// this mark for the assumption kind).
	//
	// IT IS NOT [GlyphNeedsHuman], and that is the whole reason the slot exists.
	// `?` means "waiting on a person" and is the one mark on this surface that
	// is always amber; an assumptions card is waiting on nobody — it is going
	// ahead, and the offer to strike a line is a courtesy rather than a gate.
	// Drawing it with the attention mark told a person to answer something that
	// was not asking them anything, which is the fastest way to make the amber
	// mark stop meaning what it says.
	//
	// It is also not [GlyphEstimate]. A tilde is bound to a NUMBER — 10.2.8's
	// "estimated number" — and this stands alone at the head of a card; the two
	// are near neighbours in shape and say different things, which is exactly
	// the distinction the one-glyph-one-meaning gate exists to keep.
	GlyphAssumed = "≈"

	// Disclosure and navigation.
	GlyphCollapsed = "▸"
	GlyphExpanded  = "▾"
	GlyphScopeUp   = "‹" // scope header / go up

	// GlyphPointer is THE PERSON'S POINTER ON A QUESTION: the answer `enter`
	// takes. It is the fold mark's own small triangle, and deliberately so —
	// both say "this row, and your key goes into it" — but it is a SLOT of its
	// own because a question's pointer is amber and moves under a hand, where a
	// fold mark is a dim fact about a section; one slot for both was the
	// question's page drawing `▸` for "folded" beside `▸` for "you are here"
	// (docs/design/questions/DESIGN.md, 2026-09-11).
	GlyphPointer = "▸"
	// GlyphRecommended is THE ASKER'S PICK: the answer the thing asking would
	// take, drawn at the right edge of that answer's row with the word
	// `recommended` beside it. It is amber, like the other two marks of a
	// question, and it is a filled shape where the pointer is a triangle so the
	// two can stand on one row and never be read as each other.
	GlyphRecommended = "◆"

	// The one frame (internal/tui3/frame.go): a rounded, dim edge around the
	// one object on the surface that is waiting for a person (a question), and
	// around the few sheets raised over the page on purpose. Box drawing is
	// already the right character for a grid, so these are geometry: the tier
	// never touches them, and a terminal refused box drawing gets the frame's
	// own ASCII run (two plain rules, no sides), which the frame draws itself.
	GlyphFrameTopLeft     = "╭"
	GlyphFrameTopRight    = "╮"
	GlyphFrameBottomLeft  = "╰"
	GlyphFrameBottomRight = "╯"
	GlyphFrameEdge        = "─"
	GlyphFrameSide        = "│"
	// The two junctions are where a frame's rule meets the seam between two
	// panes laid side by side (internal/tui3/panes.go): the rule above the panes
	// drops into the seam and the rule below closes it. They are the same
	// geometry as the rest of the frame, so they are drawn at the same floor.
	GlyphFrameTeeDown = "┬"
	GlyphFrameTeeUp   = "┴"

	// GlyphTarget is WHERE THE NEXT THING GOES, and it is the one mark in this
	// vocabulary about a destination rather than about a state. Home's rule wears
	// it in front of the folder and the model the next conversation will open on
	// (internal/tui3's homedraft.go), and the whole of its meaning is the
	// difference between "where I am" and "where this is going" — which is why it
	// is neither [GlyphScopeUp], a header pointing back up a tree, nor
	// [GlyphPromptSteer], a composer's own prompt.
	//
	// IT IS GEOMETRY ON PURPOSE. An arrow is already the right character for a
	// grid, exactly as the tree corners and the rails are, and a Font Awesome
	// arrow in its place would buy nothing and spend a private-use codepoint.
	GlyphTarget    = "→" // where the next thing goes
	GlyphTruncated = "⋯" // clickable overflow; [GlyphEllipsis] marks static overflow

	// GlyphEllipsis is §16's ONE ELLIPSIS GRAMMAR as a slot: the mark text
	// leaves behind when it was too long for its column. It is deliberately NOT
	// [GlyphTruncated] — an ellipsis says "the rest is off the edge", a ⋯ says
	// "there is more, click for it", and a cut ([GlyphCut]) says "this stopped
	// and should not have". Three marks, three sentences.
	//
	// It was the most-drawn mark in the product with no name here: prose,
	// placeline, modelui, palette, footer and composer each spelled the
	// byte themselves, and three of them wrote a comment explaining which of
	// the other two marks they did NOT mean. The slot ends the explaining.
	//
	// U+2026 is Ambiguous width and one cell under both shipping rulers. It has
	// no nerd-font twin on purpose: the tier's ellipsis icon is already spent on
	// [GlyphTruncated], and this mark lands inside sentences a user wrote, where
	// a rewrite would be an edit rather than a repertoire swap (12.7 D.2).
	GlyphEllipsis = "…"

	// GlyphCut is the truncation law's visible mark (12.5.2): a turn ended by
	// anything other than its own completion renders VISIBLY CUT. The severed
	// double-dash rule is deliberately NOT [GlyphTruncated] — an ellipsis says
	// "there is more, ask for it", and a cut says "this stopped and should not
	// have". Conflating the two is exactly the lie of omission 12.5 found.
	// Colour comes from [CutToken]; U+254C is Neutral width, one cell under
	// every ruler including a CJK locale.
	GlyphCut = "╌"

	// Composers. The two prompts differ so the affordance never lies about
	// which surface the draft will land in (5.15).
	GlyphPromptChat  = "›"
	GlyphPromptSteer = "↦"
	// GlyphReplyIn is AN ANSWER DRAWN UNDER THE THING IT ANSWERS: the reply to a
	// question a person put back to the asker, the response landing on the row
	// that asked for it (docs/design/questions/DESIGN.md's room form). It is a
	// prompt mark in the same family as the two above — punctuation saying whose
	// turn a line is — which is why it is geometry and neither tier swaps it.
	//
	// It is deliberately NOT [GlyphPromptChat]: `›` is the person typing and this
	// is what came back, and a page that drew both with one mark would make an
	// exchange unreadable at exactly the moment it matters. U+21B3 is
	// East_Asian_Width=Neutral and one cell under both shipping rulers.
	GlyphReplyIn = "↳"
	// GlyphDraftUnsent is a message a person typed and then CLEARED the whole
	// box into — the mark the /drafts page draws in front of each line that
	// is still waiting to come back (internal/tui3's draftpage.go).
	//
	// IT IS PENCIL-SHAPED LIKE [GlyphWrite] AND NOT THE SAME PENCIL. ✎ is the
	// step gutter's "a call wrote something down"; ✐ (U+2710 LOWER RIGHT PENCIL)
	// is a thing the hand still holds. It is also deliberately NOT the steer
	// prompt's mapped-into arrow one slot up: a draft has gone nowhere yet, it
	// sits on the wrong side of the line that one draws.
	GlyphDraftUnsent = "✐"

	// The execution voices (5.5). A work record is four speakers and no
	// labels: the model thinking, the tools it reached for, the reader
	// steering, and what came back. The reader's voice is [GlyphPromptChat]
	// — the same mark they typed at — and what came back wears the quote
	// gutter, so the slots this adds are the three the vocabulary was short:
	// the model's own thought and the two tool kinds that are not a shell.
	//
	// All three measure one cell under both shipping rulers and all three are
	// East_Asian_Width=Neutral, so they are the rare glyphs that do not even
	// cost a reserved column under a CJK locale.
	GlyphThought = "✳" // the model's own words between calls
	GlyphShell   = "$" // a shell call — the prompt a person types at
	GlyphSearch  = "⌕" // a call that went out to the world
	GlyphFilter  = "⌕" // narrowing what is already on the page
	GlyphWrite   = "✎" // a call that wrote something down

	// The action families (internal/tui3's step gutter). One still, monochrome
	// mark per FAMILY of work — searching, editing, running a command — keyed
	// off the closed vocabulary the engine carries in session.ActionCategory.
	//
	// THEY ARE SLOTS HERE AND NOT CHARACTERS THERE. The surface used to hold
	// its own three-tier table, with the private-use codepoints spelled inline
	// beside the plain marks, so ten icons and ten plain glyphs lived outside
	// the width gate, outside the ban list and outside the one-meaning law —
	// which is how ▤, ◎ and ◷ came to be drawn product-wide without ever having
	// been measured. Four of the families reuse a slot this table already owns
	// (search, write, shell, and the diff-add byte for a thing that was not
	// there); the rest are named here, and the surface keeps only the map from
	// a family to a slot.
	GlyphActionRead        = "▤" // a box with lines in it — a page of text, opened
	GlyphActionCreate      = "+" // something that was not there is
	GlyphActionTest        = "◎" // a target being aimed at — NEVER a checkmark
	GlyphActionBrowse      = "↗" // out of here and onto a page somewhere else
	GlyphActionTransfer    = "⇄" // bytes going the other way as well
	GlyphActionCommunicate = "»" // the guillemet: something being SAID, to a person
	GlyphActionCoordinate  = "⇉" // work handed out, or this mind copied to run beside itself
	GlyphActionPlan        = "≡" // three level lines, an outline
	GlyphActionWait        = "◷" // a quarter of a clock face, still
	GlyphActionWork        = "▪" // a small square: "a step", which is all it knows

	// Meta.
	GlyphBoosted   = "⇡" // transient escalation of the work-role binding (8.2.16)
	GlyphSeparator = "·" // telemetry separator
	GlyphMissing   = "—" // missing data — never an estimate (10.2.8)
	GlyphEstimate  = "~" // estimated number (10.2.8)

	// Structure (5.21). The accent rail groups a card's lines in its identity
	// hue; the drag handle marks a reorderable pending row (5.22).
	GlyphAccentRail = "▎"
	GlyphDragHandle = "⋮"

	// GlyphHugEdge is the composer hug's left edge: the one cell at column 0 of
	// the row you type into, carrying the composer's state colour.
	//
	// It is U+258D LEFT THREE EIGHTHS BLOCK and NOT [GlyphAccentRail], and the
	// distinction is the vocabulary's whole point rather than a shade of taste.
	// §4 spends `▎` on one meaning product-wide — "a finished answer" — and says
	// nothing else may wear it. The hug's edge means something else entirely:
	// "this is the live surface, and here is what it is doing right now". Two
	// meanings may not share a mark, so the hug takes the next rung of the same
	// eighth-block ladder the code gutter (`▏`) and the accent rail already
	// stand on: same family, so the three read as one system, different width,
	// so they are told apart at a glance and by the table.
	GlyphHugEdge = "▍"

	// GlyphChipCapLeft and GlyphChipCapRight are the two cells that soften a
	// filled chip's ends: U+2590 RIGHT HALF BLOCK opens it and U+258C LEFT HALF
	// BLOCK closes it, each painted with the CHIP's ground as its FOREGROUND
	// over the surface's own ground. Half a cell of chip and half a cell of
	// floor, which is as close to a rounded corner as a terminal gets.
	//
	// They are geometry rather than iconography — the shapes ARE the meaning,
	// there is nothing for a patched font to improve, and both measure one cell
	// under both shipping rulers like every other eighth/half block already in
	// this table. They are named here rather than spelled inline for §16's flat
	// reason: a mark drawn from a literal escapes the width gate, and the day a
	// second surface wants a filled chip it must get the same two cells.
	GlyphChipCapLeft  = "▐"
	GlyphChipCapRight = "▌"

	// Step dots: plan progress as one dot per step (5.21). Display only on
	// narrow rails — too small to hit honestly (5.22).
	GlyphStepDone    = "●"
	GlyphStepRunning = "◐"
	GlyphStepPending = "○"
	GlyphStepBlocked = "⚑"

	// Run progress cells summarize task state across a whole run.
	GlyphDoneCell    = "●"
	GlyphRunningCell = "◐"
	GlyphEmptyCell   = "○"
	GlyphFailedCell  = "✘"

	// Queue pills: one glyph per queued item, capped (10.3.13).
	GlyphQueuePill = "▶"

	// Diff micro-stats on settle rows (5.21), green and coral, tabular. The
	// minus is U+2212, which is one cell in every width mode and lines up with
	// the plus; ASCII '-' does not.
	GlyphDiffAdd = "+"
	GlyphDiffDel = "−"

	// The compact inline spawn tree written into the committed transcript at
	// birth and settle (10.3.11).
	GlyphTreeBranch = "├"
	GlyphTreeLast   = "└"
	GlyphTreeVert   = "│"
	GlyphTreeDash   = "─"

	// The place line (5.19): where work lands on disk. These three were drawn
	// in 5.19's own example before they had names here, and they are plain-tier
	// glyphs in their own right — the glyph TIER (12.7) upgrades them, it did
	// not invent them.
	//
	// GlyphHome is U+2302 HOUSE, Neutral width and universally covered, which
	// is why 5.19 reached for it. GlyphFolder is the ASCII slash, because a
	// slash already means "directory" in every shell anyone has ever used and
	// no font can fail to draw it. GlyphGitBranch is U+22D4 PITCHFORK, Neutral
	// and one cell; where a font cannot draw it the documented substitute is
	// ":" — the "git:main" convention — which is ASCII and the same width.
	GlyphHome      = "⌂"
	GlyphFolder    = "/"
	GlyphGitBranch = "⋔"

	// The status line (5.17's "K3 ▄ $8.65"). The model mark is U+25C7 WHITE
	// DIAMOND, one cell under both rulers and Ambiguous exactly as the gauge
	// beside it already is. The spend mark is the dollar the money cell was
	// already carrying, named so the tier can swap it as a slot rather than as
	// a substring.
	//
	// The `$` is spelled twice on purpose and it is the one collision in the
	// vocabulary worth stating out loud: [GlyphShell] is the same byte saying a
	// different thing (a shell call, 5.5). They are told apart by what follows —
	// the spend mark is bound to a number and the shell mark is followed by a
	// space — never by shape, and the nerd-font tier separates them outright
	// (nf-fa-dollar against nf-fa-terminal). glyphvocab_test.go carries the pair
	// in its named-exceptions table so a THIRD `$` slot has to be argued for.
	GlyphModel = "◇"
	GlyphSpend = "$"

	// The file kinds. A chip says WHAT KIND OF THING is on the end of a path
	// before it says the path, and so does the gutter beside a call that made
	// one or opened one; the four kinds a person can hand this program are the
	// four here.
	//
	// GlyphFileDocument is [GlyphActionRead]'s byte on purpose — a page of text
	// is a page of text whether a call opened it or a person dragged it in, and
	// the tier draws the SAME icon for both rather than inventing a distinction
	// the floor does not draw. It is the one collision in this block, and it is
	// carried in glyphvocab_test.go's named-exceptions table.
	//
	// GlyphFileVideo is U+25B7 WHITE RIGHT-POINTING TRIANGLE and NOT the filled
	// U+25B6 a chip used to draw: the filled triangle is [GlyphQueuePill]'s, one
	// plain byte may upgrade exactly one way, and an outline triangle is the
	// right weight beside three outlined file icons anyway.
	//
	// GlyphFileImage is U+233E APL FUNCTIONAL SYMBOL CIRCLE JOT and
	// GlyphFileAudio is U+266A EIGHTH NOTE, both one cell under both rulers.
	GlyphFileDocument = "▤"
	GlyphFileImage    = "⌾"
	GlyphFileAudio    = "♪"
	GlyphFileVideo    = "▷"
)

The glyph vocabulary (5.17, 5.21), and the whole of what a person sees drawn as a mark anywhere in this product. Its law, its three tiers, the table as it landed and how to add to it are docs/design/icons/DESIGN.md; the short of it is that every mark is a SLOT with a plain, a nerd-font and an ASCII spelling, that GlyphSet.Glyph(id) is the one door to them, and that a surface spelling a mark as a literal is a build failure rather than a matter of taste.

No emoji in chrome: emoji are double-width, render inconsistently, carry their own untintable colors, and read as notification confetti rather than as an instrument. Everything here is single-width, tintable and metric-safe — and glyph_test.go proves the width claim against the same library the renderer measures with, so a tempting new glyph cannot enter the vocabulary without passing the ruler.

AMENDMENT TO 5.17 (measured, not argued): the section lists ⚡ for "boosted", but ⚡ (U+26A1) has East_Asian_Width=Wide and measures TWO cells under both x/ansi and go-runewidth — it fails the very law the section states. Boost is an escalation, so it ships as ⇡ (U+21E1, one cell everywhere).

View Source
const (
	MotionIntervalMin = 100 * time.Millisecond
	MotionIntervalMax = 150 * time.Millisecond
)

MotionIntervalMin and MotionIntervalMax are the band MotionInterval must stay inside, with the reasons stated at MotionInterval. They are here so a future tuning is a review of two numbers rather than a rediscovery of why 60ms felt frantic.

View Source
const (
	MotionPeriodMin = 800 * time.Millisecond
	MotionPeriodMax = 2 * time.Second
)

MotionPeriodMin and MotionPeriodMax bound one full cycle of any motion in the table. Under 0.8s a cycle reads as urgency — a thing that wants something from you — which is amber's job and not a spinner's (see Amber); over 2s the motion stops answering "is this alive?" within the glance that asked.

View Source
const (
	PulseSteps  = 12
	PulsePeriod = PulseSteps * MotionInterval
)

PulseSteps is the breathe's period measured in house steps, and PulsePeriod is the same number as a duration: 12 × 120ms = 1.44s per full breath.

TUNED FROM 8 STEPS (960ms). At 960ms the dot cycled just under once a second, which is a resting heart rate — fast enough that a `planning…` line read as impatient, and close enough to the spinner's 1.2s rotation that the two motions beat against each other on a screen showing both. 1.44s is a slow breath, it is audibly not the spinner's period, and it sits inside MotionPeriodMin..MotionPeriodMax. 12 is also 4 frames × 3 steps, so each frame gets whole steps at flat easing and the eased version has room to linger without any frame being skipped outright.

View Source
const (
	// DimTowardGround is how far a token's dimmed variant travels toward the
	// ground. 0.45 is the point where the whole pane visibly recedes while
	// every body-class token still clears 3.0 contrast (contrast_test.go).
	DimTowardGround = 0.45

	// BandIdentityTint is how much identity hue the selection band takes when
	// the user is inside that task's scope (5.16: "the selection band tint
	// inside that task's scope"). 0.08 is the largest tint at which the chrome
	// tier still clears 3.0 on the tinted band — the tint is meant to be
	// answered peripherally, never noticed.
	BandIdentityTint = 0.08

	// SheetTowardBand is how far the floating dialog's own ground travels from
	// the [Ground] toward the [Band] — the one derivation that makes the
	// elevation ladder ground → sheet → band monotone by construction rather
	// than by three hand-picked literals.
	//
	// 0.45 is chosen by the two ends it has to satisfy at once, and both are
	// tight: below about 0.35 the sheet resolves to the SAME xterm-256
	// greyscale entry as the ground (the cube is coarse in this corner — see
	// [Sheet]) and the elevation disappears on the majority profile; above 0.5
	// the band it carries stops reading as raised against it. At 0.45 the sheet
	// is 1.09 over the ground and the band is 1.14 over the sheet, and the
	// three rungs land on three different 256 entries in the right order.
	SheetTowardBand = 0.45

	// HugBarTowardBand and HugInputTowardBand are the composer hug's two
	// grounds, on the same ground→band axis the [Sheet] is derived along and
	// deliberately BELOW it.
	//
	// The hug is the two permanent rows at the bottom of a room — the input row
	// and the bar row under it — and it is not a dialog. A dialog is a thing
	// that arrived and will leave, so it may announce itself; the hug has been
	// there since the window opened and will be there when it closes, and a
	// permanent plane painted at the dialog's rung reads as a slab welded across
	// the bottom of the screen. Two rungs under the sheet is what "the floor
	// changes here" looks like when the change is allowed to be quiet.
	//
	// WHY TWO AND WHY THESE TWO. The input row sits one shade LIGHTER than the
	// bar row, which is the whole depth cue: the row you type into is nearer,
	// the row that names where you are is further back, and neither needs a
	// hairline to say so (16's SURFACE SEAMS ARE GROUNDS, and its ban on a third
	// ruled line). The numbers are bounded on three sides at once — the pair has
	// to stay under the sheet (0.45), has to clear [HugSeparationMin] from each
	// other, and has to survive the xterm greyscale ramp, which holds exactly one
	// step between the ground's entry (233) and the band's (235). 0.10 resolves
	// to 233 and 0.40 to 234, so the two-tone is real at 256 colours as well as
	// at truecolor; a bar rung above ~0.12 collapses onto the input rung's entry
	// and the depth disappears on the majority profile.
	HugBarTowardBand   = 0.10
	HugInputTowardBand = 0.40

	// CardWorkingTowardBand and CardDeliveredTowardBand are the two card
	// grounds, on the same ground→band axis every other rung is derived along.
	//
	// §4 gives the delivery card "a distinct ground + `▎` accent left edge" and
	// says nothing else may wear that treatment. These are the planes the two
	// card states stand on; without them the card would be carried by its edge
	// alone.
	//
	// WHY TWO. A task in the conversation has two states a reader must tell
	// apart at a glance — the one just made, and the one that came back — and
	// §16 says the way to separate them is a ground shift rather than a rule.
	// So the commitment card gets its own quieter rung and the delivery card
	// gets the louder one, and the STEP between them is what says "this
	// finished". The treatment stays unique because only the delivery wears the
	// edge as well: at NoColor, where neither ground is drawn at all, the `▎` is
	// still the only thing on screen that says "a finished answer".
	//
	// The numbers are bounded on three sides, exactly as the hug's are. The
	// delivered rung stays UNDER [BandSeparationMin] (1.1424 against a 1.15
	// floor), because a card raised as far as a selection is the slab §16
	// refuses and there would be nowhere left for a band drawn on top of it. The
	// pair clears [SheetSeparationMin] from each other (1.0914), so the step is
	// a step and not a rounding. And the two land on different xterm greyscale
	// entries, which is the tight one: the ramp holds a single step between the
	// ground's entry (233) and the band's (235), so a working rung authored much
	// higher collapses onto the delivered rung's entry and the difference exists
	// only on the profile nobody screenshots.
	CardWorkingTowardBand   = 0.25
	CardDeliveredTowardBand = 0.65
)

The palette: pastel semantics on a dark ground (5.16), the three-tier grey ramp (5.13), and the selection band (5.16). Every value below is a literal so the whole palette can be read at once; every DERIVED value (dimmed variants, identity-tinted bands) is additionally re-derived from its base by palette_test.go through Mix, so a hand-edited literal that stops obeying the derivation rule fails the build.

Base ground and the derivation constants ---------------------------------------- The dark ground is near-black with a trace of blue so the pastels read as warm against it; the band is the same ground raised one step.

View Source
const (
	BandSeparationMin = 1.15
	BandSeparationMax = 1.70
)

BandSeparationMin and BandSeparationMax bound how far the selection band may sit from the ground. Below the minimum the band is invisible and selection stops reading; above the maximum it becomes a box, which 5.13 forbids ("cards separated by whitespace not boxes").

View Source
const CaretMotion = "caret"

CaretMotion names 11's third motion so the table is complete, and records that we do not own its cadence. The composer's cursor is requested from the terminal through DECSCUSR and blinks at whatever rate the user's terminal (and their own preference) says; the shell's only jobs are to ask for it and to restore the user's cursor on exit. There is deliberately no interval here to tune: a caret we drew ourselves would be a fourth animation competing with the terminal's own, and it would keep blinking in a frozen render.

View Source
const HugSeparationMin = 1.05

HugSeparationMin is the floor the composer hug's two rungs sign, and it is lower than SheetSeparationMin for a reason about EDGES rather than about standards being relaxed.

A sheet is judged across a gap: it floats over a scrolling backdrop, the eye compares two fields that are nowhere adjacent, and small differences lose to the memory of the colour that was there a moment ago. The hug's two rungs share a horizontal edge that runs the full width of the window and never moves. Two large fields meeting along a straight line is the single easiest luminance comparison the visual system makes — the edge itself does the work — so the step that reads there is smaller than the step a floating plane needs. Setting the pair at the sheet's floor would have forced at least one rung ABOVE the sheet, which is the slab this whole treatment replaced.

The rungs keep a second carrier regardless, exactly as the band on a sheet does: the input row wears the state-coloured edge glyph at column 0, so a reader on a profile that paints no ground at all still knows which row is the one they type into.

View Source
const IdentityCount = 8

IdentityCount is the size of the identity wheel. Eight is enough that no two adjacent rail cards ever need to share (the assignment in identity.go proves it) and few enough that each hue stays distinguishable.

View Source
const MotionInterval = 120 * time.Millisecond

MotionInterval is THE house animation step: every animated cell in the product advances on this grid and no surface may pick its own.

120ms, and the band around it is narrow for measured reasons at both ends. Below about 100ms a braille cycle stops reading as rotation and starts reading as noise — the eye cannot resolve ten distinct frames a second, so the row shimmers rather than turns — and every step is a repaint of every live row, so the cost is paid in wakeups the user cannot even see. Above about 150ms the same cycle visibly steps: a ten-frame spinner at 160ms takes 1.6s to come round and each frame is long enough to be read as a separate glyph rather than as one turning thing. 120ms puts the full braille rotation at SpinnerPeriod = 1.2s, which is the calm end of the legible band.

It is also the grid the phase-lock is built on: a frame is floor(now/interval) % frames, so two rows given the same latched instant show the same frame, and a repaint landing inside one step produces byte-identical rows and therefore zero dirty rows (8.1.3). A second interval anywhere in the product breaks both properties at once — the rows drift apart AND the shell starts waking on two schedules.

View Source
const NFFailedCell = "nf-fa-times_circle_o"

The Nerd Font tier's data (12.7 B.1), as one table.

PROVENANCE. Every codepoint below was verified against the glyphnames.json published by ryanoasis/nerd-fonts at **v3.2.1** (2024-04-12), a trimmed extract of which is vendored at testdata/nerdfont_glyphnames.json and walked by glyphset_test.go. The NAME is the contract and the codepoint is a binding: if a future release moves one, the test fails and the table is corrected rather than the surface quietly drawing the wrong shape. Every name in B.1 verified at its documented address, so none of the named alternates (nf-fa-cube U+F1B2 for the model mark, nf-fa-sign_in U+F090 for the steer prompt, nf-oct-git_branch U+F418 for the branch) was needed.

THREE SLOTS WERE REPICKED after B.1 shipped, by the glyph audit, and each carries its argument at its own binding rather than here: NeedsHuman (nf-fa-question_circle -> nf-fa-question_circle_o), Folder (nf-fa-folder -> nf-fa-folder_o) and Cut (nf-fa-scissors -> no icon at all). The first two are one rule applied twice — an icon inherits the INK WEIGHT of the plain glyph it stands in for, not only its meaning, its tint and its cell — and the third is the geometry rule catching a slot B.1 filed on the wrong side of it.

MEASUREMENT. Every glyph on both sides measures one cell under both shipping rulers (ansi.StringWidth, grapheme; ansi.StringWidthWc, wcwidth), and every NF codepoint is BMP private use and therefore East_Asian_Width=Ambiguous — asserted positively by the gates, because a PUA glyph that reported otherwise would mean this table had drifted.

The codepoints are written as \u escapes rather than as the characters themselves for the plainest reason there is: a private-use character is invisible in an unpatched editor, a terminal and a diff, and a table nobody can read in review is a table that drifts.

TARGET. The tier targets the **Mono** Nerd Font variants, whose icons are drawn to one cell by construction. The plain "Nerd Font" and "Nerd Font Propo" variants draw many icons at roughly two cells over a one-cell advance: the grid still advances one, so layout is safe either way, but legibility is not. That is a font choice, recorded here so the symptom is diagnosable, and named in the settings row's hint. NFFailedCell pins the Font Awesome vocabulary spelling for a failed run share.

View Source
const PulseEase = 0.6

PulseEase is how strongly the breathe's dwell bends: 0 is a flat tick, and 0.6 dwells roughly 2.5× longer mid-cycle than at the edges. It is what makes the dot BREATHE rather than count — a flat four-frame cycle at any period is a clock, and the thing being said here is "someone is thinking", not "3.6 seconds have passed". The value remains part of the table so its meaning is readable beside the frames it shapes.

View Source
const SheetSeparationMin = 1.08

SheetSeparationMin is the smallest step at which one plane reads as another plane on this ground, and it is the gate BOTH rungs of the dialog's ladder sign: the Sheet over the Ground, and the Band over the Sheet.

It is lower than BandSeparationMin on purpose. A band is a mark the eye must FIND — it says which of twenty rows the keyboard is on, unaided. A plane is a mark the eye only has to BELIEVE: it is bounded by 12.11's hairline, it is a rectangle of hundreds of cells rather than one row, and an edge between two large fields is visible far below the ratio a small mark needs. Holding a sheet to the band's floor would spend the whole ground→band range on the first rung and leave the selection nothing to be raised above.

The band on a sheet keeps its own second carrier regardless: every list on these surfaces draws GlyphAccentRail on the selected row as well, which is 12.11.2's ruling that a state colour carries applied one plane up.

SpinnerPeriod is one full rotation of SpinnerFrames at the house cadence: ten braille frames × 120ms = 1.2s. It is DERIVED rather than authored, so adding or removing a frame moves the period instead of silently changing what "one rotation" means. len() of an array is a Go constant, so this costs nothing at runtime.

Variables

View Source
var ANSI16Remap = [16]int{
	0, 9, 10, 11, 12, 13, 14, 7,
	8, 9, 10, 11, 12, 13, 14, 7,
}

ANSI16Remap is the table the sanitizer plugs in place of its Identity. It is a plain [16]int so this package stays a leaf; the call site converts:

sanitize.TextWithPalette(s, sanitize.Table(tokens.ANSI16Remap))

Entry by entry:

0  black         → 0   the ground; a tool drawing on the ground is correct
1  red           → 9   coral lives in the bright family; standard red
2  green         → 10  (#800000-ish) is illegible on a dark ground and
3  yellow        → 11  reads as a bug, not as a color choice. Every
4  blue          → 12  chromatic standard color moves to its bright
5  magenta       → 13  counterpart: same hue, same meaning, legible.
6  cyan          → 14
7  white         → 7   a tool's body text sits at our secondary tier
8  bright black  → 8   a tool's own dim text stays our chrome tier
9  bright red    → 9   the bright chromatics are already where our pastel
10 bright green  → 10  vocabulary lives; they pass through untouched, and
11 bright yellow → 11  the hue families line up one-to-one with 5.16's
12 bright blue   → 12  words (9 broken, 10 success, 11 needs-a-human,
13 bright magenta→ 13  14 alive; 12 and 13 have no semantic word and read
14 bright cyan   → 14  as identity-ish, which is the honest reading of a
15 bright white  → 7   tool's own accent).

The one demotion is 15 → 7: bright white is a tool shouting, and the primary text tier is reserved for our own voice (5.13's hierarchy is only a hierarchy if nothing else may occupy its top). A tool's emphasis survives as position and as its own internal contrast against 8; it just stops outranking the sentence the user is reading.

View Source
var BannedGlyphs = []struct {
	Rune   rune
	Reason string
}{
	{'⏸', "media-control pictograph: width-unstable, emoji-presentation in many fonts (5.17)"},
	{'⏵', "media-control pictograph: width-unstable (5.17)"},
	{'⏹', "media-control pictograph: width-unstable (5.17)"},
	{'⏯', "media-control pictograph: width-unstable (5.17)"},
	{'⏭', "media-control pictograph: width-unstable (5.17)"},
	{'⏮', "media-control pictograph: width-unstable (5.17)"},
	{'⌛', "hourglass: two cells, and it lies about liveness on a detached row (8.1.6)"},
	{'⏳', "hourglass: two cells (5.17)"},
	{'⚡', "measured two cells under x/ansi and go-runewidth; 5.17's own width law refuses it"},
	{'☰', "measured two cells; also a hamburger menu, which this surface does not have"},
	{'★', "dingbat: Ambiguous width and no meaning in the five-word vocabulary (5.16)"},
	{'❯', "powerline-adjacent prompt chevron: font-fragile (8.3); the prompt is ›"},
	{'\uFE0F', "variation selector: forces emoji presentation and desynchronizes width"},

	{'\uE0B0', "powerline separator: a shape-join that needs a neighbouring background to tile against (8.3); the separator is ·"},
	{'\uE0B1', "powerline thin separator: same join, same refusal"},
	{'\uE0B2', "powerline separator, left-facing: same join, same refusal"},
	{'\uE0B3', "powerline thin separator, left-facing: same join, same refusal"},
	{'\uE0B8', "powerline slant seam: same join, same refusal"},
	{'\uE0B9', "powerline slant seam: same join, same refusal"},
	{'\uE0BA', "powerline slant seam: same join, same refusal"},
	{'\uE0BB', "powerline slant seam: same join, same refusal"},
	{'\uE0BC', "powerline slant seam: same join, same refusal"},
	{'\uE0BD', "powerline slant seam: same join, same refusal"},
	{'\uE0BE', "powerline slant seam: same join, same refusal"},
	{'\uE0BF', "powerline slant seam: same join, same refusal"},
}

BannedGlyphs are runes that may never appear in chrome, with the reason each one is out. glyph_test.go fails if any of them turns up anywhere in this package's vocabulary — the ban is enforced by the build, not by review.

The general rules the list instantiates (also enforced by the test, so a rune not named here cannot sneak in either): nothing wider than one cell, nothing in the emoji planes, nothing carrying a variation selector.

View Source
var GaugeCells = [5]string{"▁", "▂", "▄", "▆", "█"}

GaugeCells is the one-cell context gauge (5.17): context % as a single eighth-block, so "K3 ▄ $8.65" reads as "half the window gone" at a glance and costs one column. The precise percentage belongs to the focused-card tier.

View Source
var GaugeThresholds = [len(GaugeCells)]float64{0, 0.2, 0.4, 0.6, 0.8}

GaugeThresholds is the context gauge's cell ladder (5.17): the fraction at which each cell of GaugeCells takes over. The rungs are even fifths, which is the honest choice for a five-cell eighth-block ramp — the cells are themselves a linear height ramp, so a non-linear threshold table would draw a bar that disagrees with its own height.

The ladder is a TABLE rather than an arithmetic expression inside Gauge for the reason the rest of this package is tables: a threshold that only exists as `int(fraction * 5)` cannot be read by a designer, cannot be quoted in 18, and cannot be changed without re-deriving what the old numbers were.

It deliberately makes no colour judgement. The gauge's HEIGHT is a linear reading of how full the window is; whether a human needs to act belongs to the surface that has the window's current policy.

View Source
var PulseFrames = [4]string{"·", "•", "●", "•"}

PulseFrames is the breathe: the three-tier dot the `planning…` line wears while the model is thinking, up and back down.

The set is a SIZE ramp on one shape, not four different marks. That is the whole design: a growing and shrinking dot reads as breathing, while four distinct glyphs read as a second spinner, and 11 already spent its spinner. The cycle is written out rather than mirrored in code (·, •, ●, • rather than three frames walked forward and back) because the eased dwell indexes a flat list, and a list that says exactly what is drawn is a list a designer can read.

BYTE COLLISIONS, DELIBERATE: `·` is also GlyphSeparator and GlyphProseBullet; `●` is also GlyphStepDone. The glyph lane ruled the collisions acceptable — a slot is a meaning and not a byte, and these three are the honest small/medium/large dots in a repertoire every terminal has — so they are named here as their own slots and walked by the width gate under their own names (see Glyphs).

WIDTH: all three are East_Asian_Width=Ambiguous, which is the property that matters. Not "all narrow" — all THE SAME, together, under both rulers, so the row cannot change width mid-breath under a CJK locale the way 5.21's ◐◓◑◒ spinner would have. glyph_test.go's TestAnimatedSetsAgreeOnWidth measures it.

View Source
var SparklineCells = [7]string{"⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"}

SparklineCells is the braille burn-trend ramp for a focused card's cost or token history (5.21) — six to eight cells, one row.

View Source
var SpinnerFrames = [10]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}

SpinnerFrames is the shared-clock spinner for TRANSIENT tool rows only (8.1.6): rows that live for seconds, where motion is honest. Never on a rail card or an agent row.

AMENDMENT TO 5.21: the section proposes ◐◓◑◒ as the rotating glyph. Those four disagree about East-Asian width — ◐ and ◑ are Ambiguous (two cells under a CJK-locale terminal), ◓ and ◒ are not — so the set would make a spinning row change width mid-spin, which is exactly the width instability 5.17 bans. Braille is width-homogeneous under every mode and is already the house spinner.

Functions

func AppendContext

func AppendContext(dst []byte, used, window int64) []byte

AppendContext writes the context form (8.2.20): "5.1%/1M". The window rides along because a percentage of an unnamed window is not information — 5% of 1M and 5% of 128K are different situations. A window of zero renders GlyphMissing: unknown is not zero.

func AppendContextCell

func AppendContextCell(dst []byte, used, window int64) []byte

AppendContextCell is AppendContext right-aligned in ContextCellWidth.

func AppendCount

func AppendCount(dst []byte, n int64) []byte

AppendCount writes the number ladder (8.2.20): 999, 1.5K, 25K, 1M. A rung with a leading digit under ten keeps one decimal; above that the decimal is noise on a number nobody reads that precisely. A trailing ".0" is always dropped, so a round million is "1M".

func AppendCountCell

func AppendCountCell(dst []byte, n int64) []byte

AppendCountCell is AppendCount right-aligned in CountCellWidth.

func AppendDuration

func AppendDuration(dst []byte, d time.Duration) []byte

AppendDuration writes the duration ladder (8.2.20): 4.5s, 3m12s, 2h14m. Sub-minute durations keep a tenth because that is the resolution at which a person judges "fast"; above a minute the seconds are structure, not precision, so they are truncated rather than rounded — an elapsed reading that rounds up has told a small lie about work that has not happened yet.

func AppendDurationCell

func AppendDurationCell(dst []byte, d time.Duration) []byte

AppendDurationCell is AppendDuration right-aligned in DurationCellWidth.

func AppendElapsed

func AppendElapsed(dst []byte, d time.Duration) []byte

AppendElapsed writes the aging form (5.21): 45s, 5m, 1h02, 99d23. The unit granularity switches at fixed width and the trailing unit is dropped once the reading has two parts, so a row that crosses an hour does not shove the column beside it. This is the form for live regions only — a committed transcript row freezes its elapsed at finalization (8.1.2) and never ages.

func AppendElapsedCell

func AppendElapsedCell(dst []byte, d time.Duration) []byte

AppendElapsedCell is AppendElapsed right-aligned in ElapsedCellWidth.

func AppendEstimate

func AppendEstimate(dst []byte) []byte

AppendEstimate writes the honesty mark (10.2.8): a "~" before an estimated number. Callers write the value cell immediately after it, and size the column with EstimateWidth so an estimate and a measurement line up.

func AppendMissingCell

func AppendMissingCell(dst []byte, width int) []byte

AppendMissingCell writes GlyphMissing right-aligned in width cells. Missing data renders "—", never an estimate (10.2.8) and never a zero: a number that has not arrived and a number that is zero are different facts.

func AppendMoney

func AppendMoney(dst []byte, usd float64) []byte

AppendMoney writes money at the resolution the TUI reads it: four decimals below a cent, exact cents up to $999.99, then the count ladder ("$1.2K"). This is the surface form; the prompt form is AppendMoneyDime, and the difference is deliberate — see that function.

THE SUB-CENT RUNG (12.9.2, adopted here from the head at 12.10.6's finding). Two decimals turned a measured $0.0017 into "$0.00", and "$0.00" does not read as "very small" — it reads as FREE. 12.9.2 found that at its worst: a model shown a rate it had been told was zero reached for a figure that was not, and wrote "$20.00 a run". The head's moneyUSD learned to render at the precision a figure actually has; the token layer did not follow, so every surface reading a real rate under half a cent showed nothing. It follows now.

The law the rung keeps, stated as the invariant the test pins: A POSITIVE FIGURE NEVER RENDERS AS ZERO. Below the fourth decimal the ladder has run out of digits, and it spends its last one rather than rounding down into a lie — $0.00001 renders "$0.0001". That is an overstatement bounded by one hundredth of a cent, and it is the honest direction to be wrong in: the reader learns "smaller than this instrument resolves", never "free". Exact zero is still "$0.00", because zero is a fact and not a rounding.

func AppendMoneyCell

func AppendMoneyCell(dst []byte, usd float64) []byte

AppendMoneyCell is AppendMoney right-aligned in MoneyCellWidth.

func AppendMoneyDime

func AppendMoneyDime(dst []byte, usd float64) []byte

AppendMoneyDime mirrors the head's dimeUSD idiom exactly: round to a dime, print two decimals ($8.65 → "$8.70"). Position by volatility applies to precision as well as to order — a figure may only be as precise as it is stable, and a cent on a live job ticks constantly while nobody cancels a job over three cents.

Where each form belongs: dimes for anything written into a model's context or a board that is rewritten on every turn (the head's rule, and the reason it exists); exact cents for anything a person reads as a number — the TUI, receipts, the store.

func AppendPercent

func AppendPercent(dst []byte, fraction float64) []byte

AppendPercent writes a fraction as a percentage: 5.1% below ten, 62% above, 100% at the top. One decimal under ten because that is where a tenth of a point is still a visible amount of context; above it the decimal is churn.

func AppendPercentCell

func AppendPercentCell(dst []byte, fraction float64) []byte

AppendPercentCell is AppendPercent right-aligned in PercentCellWidth. It pads by CELLS, not by bytes: AppendPercent renders GlyphMissing for a fraction that does not exist, and that mark is three bytes wide and one cell wide. Padding it by byte length would leave the column two cells short — which is exactly the dancing-neighbour bug 5.21 forbids, arriving only when a number goes missing.

func CodeHighlighting

func CodeHighlighting(p Profile) bool

CodeHighlighting reports whether syntax colour survives this profile. A renderer that gets false must draw the whole block at the primary text tier rather than pick six of the classic sixteen — see the file comment.

func Context

func Context(used, window int64) string

Context is AppendContext as a string.

func ContextCell

func ContextCell(used, window int64) string

ContextCell is AppendContextCell as a string.

func Contrast

func Contrast(a, b Color) float64

Contrast is the WCAG contrast ratio between two colors, in [1, 21]. The palette's shipping gate (contrast_test.go) is expressed in these numbers: 4.5 is AA for body text, 3.0 is AA for large text and for non-text UI components.

func Count

func Count(n int64) string

Count is AppendCount as a string.

func CountCell

func CountCell(n int64) string

CountCell is AppendCountCell as a string.

func Duration

func Duration(d time.Duration) string

Duration is AppendDuration as a string.

func DurationCell

func DurationCell(d time.Duration) string

DurationCell is AppendDurationCell as a string.

func Elapsed

func Elapsed(d time.Duration) string

Elapsed is AppendElapsed as a string.

func ElapsedCell

func ElapsedCell(d time.Duration) string

ElapsedCell is AppendElapsedCell as a string.

func EstimateWidth

func EstimateWidth(width int) int

EstimateWidth is the column width a cell needs when its values may be estimates: one more, for the "~".

func Gauge

func Gauge(fraction float64) string

Gauge maps a fraction in [0,1] to one cell of GaugeCells by walking GaugeThresholds — the ladder is a table so it can be read and quoted, not an arithmetic expression only the compiler sees. Out-of-range values clamp: a gauge that ran past its window still reads full rather than panicking a render, and a reading that does not exist (NaN) reads empty rather than guessing (16's EMPTINESS).

The cell says HOW FULL. It never says whether that is a problem; colour is a separate judgement and deliberately not a sixth cell.

func GlyphProbeLine

func GlyphProbeLine(g GlyphSet) string

GlyphProbeLine is the sample the settings sheet's live preview renders in each tier, and the line the first-run probe (12.7 E.4) will ask its one question with. Four glyphs, one space apart: a flag, a chevron, a check and a boost — chosen because they are four DIFFERENT shapes, so a font that has patched some of the repertoire and not the rest shows it here rather than at the moment a card settles.

It is a sample of the vocabulary and not a font test: rendered in a terminal without a patched font, the nerd-font line is exactly the tofu the user is being asked about, which is the point.

func IdentityIndex

func IdentityIndex(t Token) (int, bool)

IdentityIndex returns the wheel index of an identity or identity-band token, and false for anything else.

func Legal(fg Token, ff Focus, ground Token, gf Focus) bool

Legal states the composition law. There are exactly three rules, and each one is a design decision, not an accident of what happened to pass:

  1. A surface token is never a foreground, and a non-surface token is never a ground. Backgrounds and text are different vocabularies.
  2. A focused foreground may sit on the ground, on the Sheet, on the plain band, or on any identity-tinted band. Every combination is drawn in practice — any tier of text can land on a selected rail row, and the whole grey ramp lands on a dialog's own ground — and all are gated.
  3. A DIMMED foreground may sit only on the ground. This is the design law that keeps the dim state honest: an unfocused pane draws no selection band at all — it marks its selection with the dim identity accent rail ▎ (5.21) — because a band is a focus artifact, and a dimmed pastel on a raised background is the one combination in this palette that cannot clear its gate. Forbidding it is cheaper and truer than brightening every dim value until an invisible pane stops being invisible.

func MissingCell

func MissingCell(width int) string

MissingCell is AppendMissingCell as a string.

func Money

func Money(usd float64) string

Money is AppendMoney as a string.

func MoneyCell

func MoneyCell(usd float64) string

MoneyCell is AppendMoneyCell as a string.

func MoneyDime

func MoneyDime(usd float64) string

MoneyDime is AppendMoneyDime as a string.

func Percent

func Percent(fraction float64) string

Percent is AppendPercent as a string.

func PercentCell

func PercentCell(fraction float64) string

PercentCell is AppendPercentCell as a string.

func Reset

func Reset(p Profile) string

Reset clears all SGR attributes.

func ResetBg

func ResetBg(p Profile) string

ResetBg clears the background only.

func ResetFg

func ResetFg(p Profile) string

ResetFg, ResetBg, and Reset are the counterparts to Token.Fg and Token.Bg. Reset clears every attribute; the narrower two exist because a row that only set a foreground should not also clear a caller's bold.

func Reverse

func Reverse(p Profile) string

Reverse is SGR 7, the selection idiom for SelectionReverse.

func Sparkline

func Sparkline(fraction float64) string

Sparkline maps a fraction in [0,1] to one cell of SparklineCells.

func Spinner

func Spinner(tick int) string

Spinner picks the frame for a moment on the ONE shared animation clock (8.1.3): frame = floor(now/interval) % frames, so every live glyph on the screen ticks in lockstep as one organism instead of N competing pulses. Callers pass the already-divided tick, not a time — the clock lives above this package, and a token layer that read the wall clock would be a token layer with a state.

Types

type Class

type Class uint8

Class is the contrast contract a token signs. The gate in Class.MinContrast is what contrast_test.go enforces over Pairings.

const (
	// ClassBody is text and glyphs that carry meaning: the primary tier and
	// every semantic and identity hue. WCAG AA for body text.
	ClassBody Class = iota
	// ClassSupport is the secondary tier — status lines and receipts. Same
	// gate as body: it is still prose a person reads.
	ClassSupport
	// ClassChrome is the tertiary tier: telemetry, separators, hints. Gated at
	// the AA large-text / non-text-component ratio, because it is scanned, not
	// read, and 5.13 requires it to recede. Chrome that is also a button
	// brightens one tier on focus (5.22) — see [Promote] — so no interactive
	// control ever lives permanently at this gate.
	ClassChrome
	// ClassSurface is a background. Surfaces are gated on separation from the
	// ground instead of on contrast (see [BandSeparationMin]).
	ClassSurface
)

func (Class) MinContrast

func (c Class) MinContrast(f Focus) float64

MinContrast is the shipping gate for a class in a focus state.

Base values must clear AA (4.5) as body text; chrome clears the 3.0 gate WCAG uses for large text and non-text UI components. Dimmed values mark a pane the user is deliberately not reading, so they are gated one rung lower — with the standing rule that a dimmed token is never the only carrier of a piece of information (the row still has its shape, its glyph, and its place).

type CodeSlot

type CodeSlot uint8

CodeSlot names one class of source token. Six pastels plus the body tier is the whole vocabulary: the ramp answers "what KIND of word is this", not "what does this identifier mean", so a finer palette would be a palette nobody can read at prose size.

const (
	// CodeText is everything a lexer has no opinion about — punctuation,
	// operators, identifiers, whitespace. It is the primary text tier by
	// value, so unhighlighted code and the uncoloured parts of highlighted
	// code are the same grey.
	CodeText CodeSlot = iota
	CodeKeyword
	CodeString
	CodeNumber
	// CodeComment is the only slot in the ramp that RECEDES. It signs
	// [ClassChrome], because a comment is prose a reader skips past on the way
	// to the code, and 5.13 spends its contrast budget on what is being read.
	CodeComment
	CodeFunction
	CodeType
)

func CodeSlots

func CodeSlots() []CodeSlot

CodeSlots returns every slot in ramp order, for the contrast walk and for a palette-preview screen.

func (CodeSlot) Class

func (s CodeSlot) Class() Class

Class is the contrast contract this slot signs.

func (CodeSlot) Color

func (s CodeSlot) Color(f Focus) Color

Color returns the slot's value in a focus state.

func (CodeSlot) Fg

func (s CodeSlot) Fg(p Profile, f Focus) string

Fg is the precomputed SGR sequence that sets this slot as the foreground. It is "" under every profile where CodeHighlighting is false, which is what makes "ask the profile, then paint" and "just paint" agree: a caller that forgets to ask still cannot emit a colour that does not exist.

func (CodeSlot) Hex

func (s CodeSlot) Hex(f Focus) string

Hex returns the slot's value as "#RRGGBB".

func (CodeSlot) String

func (s CodeSlot) String() string

String is the slot's stable name ("code.keyword"). Golden tests key on these, so they are part of the API.

type Color

type Color struct{ R, G, B uint8 }

Color is a 24-bit sRGB value. It is a comparable value type with no pointer inside it, so passing one costs nothing and a table of them is contiguous.

func Mix

func Mix(a, b Color, t float64) Color

Mix blends a toward b by t in [0,1] in sRGB byte space, rounding half away from zero. Byte-space blending (rather than linear-light blending) is deliberate: it is what a terminal user perceives as "the same color, quieter", and it is the rule the shipped dimmed and tinted literals in palette.go were derived with — palette_test.go re-derives them through this function, so the data and the law can never drift apart.

func MustHex

func MustHex(s string) Color

MustHex parses "#RRGGBB" and panics on anything else. It exists so the palette can be written as literal hex — the form a designer reads — while still being one canonical type in memory. Every call site is a package-level constant string in this file's neighbourhood, so a panic here is a build-time error in practice, never a runtime one.

func ParseHex

func ParseHex(s string) (Color, bool)

ParseHex parses "#RRGGBB" or "RRGGBB", case-insensitively.

func (Color) AppendHex

func (c Color) AppendHex(dst []byte) []byte

AppendHex writes c as "#RRGGBB" (upper case, the form used in this package's literals) and returns the extended buffer. It allocates nothing when dst has room.

func (Color) Hex

func (c Color) Hex() string

Hex renders c as "#RRGGBB". Consumers building lipgloss styles want this form; consumers writing escape sequences want Token.Fg instead, which is precomputed and needs no parsing on the other side.

func (Color) Luminance

func (c Color) Luminance() float64

Luminance is the WCAG relative luminance of c: the sRGB channels linearized and weighted for human sensitivity. Used only by tests and by callers that want to sort colors by perceived brightness.

type CutKind

type CutKind uint8

CutKind is why a stream stopped. It mirrors the reasons a provider can hand back (finish_reason) plus the one the user causes.

const (
	// CutNone is a turn that ended by finishing. Nothing is drawn.
	CutNone CutKind = iota
	// CutLengthCap is an output token cap reached mid-answer — the exact shape
	// of session bd3c78ed (12.5), where 600 completion tokens truncated an SVG
	// and the result was journaled as if complete.
	CutLengthCap
	// CutStreamDrop is a connection or provider failure mid-stream.
	CutStreamDrop
	// CutInterrupt is the user pressing esc on a turn they were watching
	// (8.2.21). It is a record of an intent that was carried out.
	CutInterrupt
)

func (CutKind) String

func (c CutKind) String() string

String names the cut kind.

type Env

type Env func(name string) string

Env reads one environment variable. Detection takes it as a parameter rather than calling os.Getenv so the decision table is a pure function and the tests below it are a table, not a fixture.

type Focus

type Focus uint8

Focus is whether the pane owning a row currently has the user's attention. Dimming is a property of the pane (8.3: "dim/tint global chrome while scoped so you always know which room you're in"), never of the datum.

const (
	FocusNormal Focus = iota // the pane the user is in
	FocusDimmed              // an unfocused pane

)

func (Focus) String

func (f Focus) String() string

String names the focus. contrast_test.go prints it, and the golden harness keys on it, so it is part of the API.

type GlyphBinding

type GlyphBinding struct {
	ID GlyphID
	// Name is the slot's name, matching the [GlyphInfo.Name] the width gate
	// already walks.
	Name string
	// Meaning is the 5.17 meaning, carried verbatim, so the `?` help surface
	// and a glyph-preview screen read the vocabulary out of the table rather
	// than out of a second prose list that can drift.
	Meaning string
	// Plain is the 5.17 glyph and is ALWAYS non-empty: the fallback is not a
	// degradation, it is the floor.
	Plain string
	// NerdFont is the tier's icon, empty exactly when [GlyphBinding.Geometry]
	// is true.
	NerdFont string
	// ASCII is the [ASCII] tier's spelling: ONE character a screen reader can
	// name, for a slot whose plain glyph carries its meaning by shape. It is
	// non-empty for every icon slot and empty for every geometry slot, which
	// falls back to [GlyphBinding.Plain] — glyphvocab_test.go pins both halves.
	ASCII string
	// NFName is the Nerd Fonts class name — "nf-fa-adjust". The NAME is the
	// contract and the codepoint is a binding verified against the pinned
	// glyphnames extract in testdata (12.7 B.4).
	NFName string
	// UsualTint documents the token the slot is normally painted with. It is
	// documentation, not a binding: tinting stays a pure product of the state ×
	// hue axes through [ResolveToken], which is the whole reason a mono icon is
	// admissible where an emoji is not.
	UsualTint Token
	// PlainAmbiguous and NFAmbiguous record East_Asian_Width=Ambiguous on each
	// side. ALL of private use is Ambiguous, so the NF side is always true —
	// which is exactly why a CJK locale vetoes the tier (12.7 B.3, E.2): under
	// ambiguous-wide the icons draw at two cells while several plain glyphs
	// draw at one, and width parity, which holds under both shipping rulers,
	// would break.
	PlainAmbiguous bool
	NFAmbiguous    bool
	// Geometry marks a slot the tier deliberately does not touch: line
	// geometry, where box drawing and block elements are already the right
	// characters for a grid.
	Geometry bool
	// AutoUpgrade allows [GlyphSet.Upgrade] to rewrite this slot's plain glyph
	// when it arrives as a whole painted cell. It is FALSE for every slot whose
	// plain glyph is ASCII, because a painted cell that is exactly "?" or "$"
	// is plausible CONTENT — 5.20 rule 3 makes "?" a thing a user types — and a
	// mechanism that rewrote it would be a mechanism that can lie. ASCII slots
	// are adopted explicitly, through [GlyphSet.Glyph], by the one consumer
	// that owns each.
	AutoUpgrade bool
}

GlyphBinding is one slot of the vocabulary: its meaning, the 5.17 character that always says it, and the tier's icon when there is one.

func Vocabulary

func Vocabulary() []GlyphBinding

Vocabulary returns every slot in declaration order. Tests, the `?` help surface and a glyph-preview screen all walk it. The slice shares the package's table and is not to be written to.

type GlyphID

type GlyphID uint8

GlyphID names one vocabulary SLOT — the meaning, independent of tier. It is the door a consumer uses when it wants "the working glyph" rather than a particular character, and it is the only door an ASCII slot has (see GlyphBinding.AutoUpgrade).

const (
	GQueued GlyphID = iota
	GWorking
	GSettled
	GFailed
	GStopped
	GPaused
	GNeedsHuman
	GWaitsOn
	GWithdrawn
	GAssumed
	GCollapsed
	GExpanded
	GScopeUp
	GPointer
	GRecommended
	GFrameTopLeft
	GFrameTopRight
	GFrameBottomLeft
	GFrameBottomRight
	GFrameEdge
	GFrameSide
	GFrameTeeDown
	GFrameTeeUp
	GTarget
	GTruncated
	GEllipsis
	GCut
	GPromptChat
	GPromptSteer
	GReplyIn
	GThought
	GShell
	GSearch
	GWrite
	GActionRead
	GActionCreate
	GActionTest
	GActionBrowse
	GActionTransfer
	GActionCommunicate
	GActionCoordinate
	GActionPlan
	GActionWait
	GActionWork
	GBoosted
	GSeparator
	GMissing
	GEstimate
	GAccentRail
	GDragHandle
	GStepDone
	GStepRunning
	GStepPending
	GStepBlocked
	GDoneCell
	GRunningCell
	GEmptyCell
	GFailedCell
	GQueuePill
	GDiffAdd
	GDiffDel
	GTreeBranch
	GTreeLast
	GTreeVert
	GTreeDash
	GHome
	GFolder
	GGitBranch
	GModel
	GSpend
	GFileDocument
	GFileImage
	GFileAudio
	GFileVideo
	GProseBullet
	GProseQuote
	GCodeGutter
	// GFilter is the mark in front of a box that NARROWS WHAT IS ALREADY HERE,
	// and it is deliberately not [GSearch] — which means a call that went out to
	// the WORLD. The two draw the same shape because they are the same idea at
	// two distances, and they part company at the ASCII tier: a screen reader
	// hears `?` for a search, and `?` is already the mark for work waiting on a
	// person, which is a row the filter box is very often sitting above.
	GFilter
	// GDraftUnsent is the mark of a message typed and then cleared before it
	// was sent: the ring of them behind /drafts and the ↑ walk's dim end
	// (internal/tui3's draftring.go, draftpage.go).
	GDraftUnsent
)

type GlyphInfo

type GlyphInfo struct {
	// Name is the constant's name without the "Glyph" prefix.
	Name string
	// Glyph is the character itself.
	Glyph string
	// Rune is the single rune it consists of.
	Rune rune
	// AmbiguousWidth records that this rune's East_Asian_Width is Ambiguous:
	// one cell in every terminal we render for (x/ansi and go-runewidth both
	// measure it as one), but two cells in a terminal running a CJK locale
	// with ambiguous-wide enabled. The existing chat already fights this
	// (clampNodeLines sacrifices a column when it detects one). The flag is
	// exported so a shell can reserve that column deliberately instead of
	// discovering the ghost at runtime; glyph_test.go verifies every flag
	// against the width library, so this data cannot go stale.
	AmbiguousWidth bool
}

GlyphInfo is one row of the vocabulary with its measured properties.

func Glyphs

func Glyphs() []GlyphInfo

Glyphs returns the whole vocabulary, in the order it is declared above. Tests, the `?` help surface, and a glyph-preview screen all walk it.

func GlyphsIn

func GlyphsIn(g GlyphSet) []GlyphInfo

GlyphsIn is the width gate's walk, per tier.

For Plain it is the whole 5.17 floor, Glyphs exactly — every slot plus the animated sets. For NerdFont it is the icons the tier INTRODUCES: one entry per upgradable slot, and nothing else, because the geometry slots and the animated sets have no NF side at all (12.7 B.2) and walking their plain characters again under a tier name would say the tier drew something it does not draw.

type GlyphSet

type GlyphSet uint8

GlyphSet is the glyph repertoire tier: which characters say the 5.17 meanings. It composes with Profile and Focus and changes neither.

const (
	// Plain is the 5.17 floor: metric-safe in every terminal, and a designed
	// floor rather than a degradation. A user who turns the tier off gets 5.17
	// exactly as the doc specified it — same segments, same order, same tints,
	// same widths.
	Plain GlyphSet = iota
	// NerdFont is the patched-font tier: one BMP private-use icon per
	// upgradable slot, each inheriting its meaning, its tint token and its cell
	// budget from the plain glyph it replaces.
	NerdFont
	// ASCII is the floor UNDER the floor: the spelling for a screen reader and
	// for a terminal with no Unicode at all, one character a reader can NAME
	// where the other two tiers draw a shape.
	//
	// IT IS NOT SOMETHING [DetectGlyphSet] EVER RETURNS. A font is a guess and
	// a repertoire is a detection, but "this surface is being read aloud" is a
	// thing the person said out loud (the linear option) — so the shell asks
	// for this tier by name and nothing infers it. Only the ICON slots carry an
	// ASCII spelling; a geometry slot resolves to its plain character here,
	// because the ASCII spelling of a grid is a RUN of characters ("+-> ") that
	// belongs to the renderer drawing the run, not one cell in a table.
	ASCII
)

func DetectGlyphSet

func DetectGlyphSet(env Env) (GlyphSet, string)

DetectGlyphSet returns the tier a terminal should start in and the reason, for the log line. It never confirms — a positive signal cannot exist — so the answer is NerdFont unless one of the vetoes below fires.

The vetoes, and what each costs when it is wrong:

  1. TERM unset or dumb. No capability claim at all; already the NoColor floor. Costs nothing.
  2. TERM=linux. The Linux console runs a 256/512-glyph bitmap font and CANNOT render private use. This one is certain.
  3. TERM_PROGRAM=Apple_Terminal. Terminal.app ships SF Mono and Menlo, and its users are the population least likely to have patched a font. DetectProfile already special-cases it for colour. The cost is a false negative for the rare Terminal.app user who did patch one; they set the flag once.
  4. An East-Asian locale in LC_ALL, LC_CTYPE or LANG. All of private use is East_Asian_Width=Ambiguous (12.7 B.3), so a terminal running ambiguous-wide draws every icon at two cells while the plain tier draws several of them at one: width parity holds under both rulers we ship against and breaks there. The cost is a false negative for a CJK-locale user whose terminal does not run ambiguous-wide; they set the flag once.
  5. A legacy Windows console. Its font fallback for private use is unreliable. Windows Terminal (WT_SESSION) and ConEmu say so themselves and are not vetoed.

Two things that are deliberately NOT vetoes, and one that is deliberately not a confirmation:

  • tmux and screen pass the font straight through, because the font belongs to the outer terminal. This differs from COLORTERM, which inside a multiplexer is the multiplexer's claim about itself (10.1.2): the colour ladder caps under a multiplexer and the glyph ladder must not.
  • NO_COLOR is about colour. A user who wants no colour has said nothing about their font, and reading it as a glyph veto would be this package inventing a meaning for somebody else's convention.
  • TERM_PROGRAM ∈ {WezTerm, ghostty, iTerm.app, WarpTerminal}, KITTY_WINDOW_ID, WEZTERM_EXECUTABLE, GHOSTTY_RESOURCES_DIR, ALACRITTY_WINDOW_ID and LC_TERMINAL each say which TERMINAL is running and nothing about which font it was configured with. A WezTerm user on stock JetBrains Mono is a false positive, and false positives are precisely the tofu case. Since the default is already on, a positive signal buys nothing anyway — which is the tidy argument for never reading them at all.

func ParseGlyphSet

func ParseGlyphSet(s string) (GlyphSet, bool)

ParseGlyphSet parses a tier by name, reporting whether the spelling was recognized. An unrecognized spelling reports false rather than guessing, because an override that silently did something else would be the affordance lying about what it accepted (5.20).

func (GlyphSet) Glyph

func (g GlyphSet) Glyph(id GlyphID) string

Glyph resolves a slot under a tier: one array index. An out-of-range tier reads as Plain and an out-of-range slot returns "" — a render must not die because a caller handed it a number.

func (GlyphSet) String

func (g GlyphSet) String() string

String names the tier. These are also spellings ParseGlyphSet accepts, so a settings row, a flag and a log line share one vocabulary.

func (GlyphSet) Upgrade

func (g GlyphSet) Upgrade(cell string) string

Upgrade is the automatic path (12.7 D.2), and it is the identity function for Plain.

It rewrites a painted cell into this tier's glyph under ONE precise condition: the entire string is exactly one rune, and that rune is an GlyphBinding.AutoUpgrade slot's plain glyph. Never a substring rewrite anywhere in a line, and never an ASCII slot.

The warrant for the whole-cell rule is the renderer grammar: a glyph cell is painted as its own span, so it arrives at a Styler as a one-rune string. A surface built before this tier existed therefore gets the tier for free.

func (GlyphSet) UpgradeChrome

func (g GlyphSet) UpgradeChrome(cell string) string

UpgradeChrome is the EXPLICIT door for a chrome string that leads with a glyph and then says something — "▸ 12 lines" is the case it exists for. It upgrades a whole cell exactly as GlyphSet.Upgrade does, and additionally rewrites a leading glyph that is followed by a space.

It is deliberately not automatic for all chrome strings. Chrome can carry content read out of a journal, and an automatic lead-rune rewrite would edit a user's sentence. D.2 named the remedy for exactly this boundary: the rule is explicit and callers that want it ask for it by name.

type Hue

type Hue uint8

Hue is the five-word colour vocabulary of 5.16. Everything not carrying a hue is the three-tier grey ramp of 5.13.

The ordinals keep the vocabulary compact and stable inside this package; the composition rule below is the only door that interprets them.

const (
	// HueNone leaves the grey ramp alone. Zero value: an unhued cell is the
	// default, which is the point of a five-word vocabulary.
	HueNone Hue = iota
	// HueAttention (soft amber) means a human is needed: question badges,
	// waiting states, the context meter past its warn point.
	HueAttention
	// HueAlive (soft cyan) means working: live glyphs, the stream caret, the
	// thinking pulse.
	HueAlive
	// HueMoney (soft green) means money and success: cost figures, settled ✓.
	HueMoney
	// HueBroken (soft coral) means failed or cancelled.
	HueBroken
	// HueIdentity is the per-task pastel. It is NOT one colour: the actual
	// pastel comes from an identity chosen from the eight-token wheel. Resolving
	// it without a chosen identity yields the first wheel entry, which is a
	// deliberate, visible placeholder rather than a panic.
	HueIdentity
)

func CutHue

func CutHue(c CutKind) Hue

CutHue is CutToken on the hue axis, for a renderer painting through the Styler seam rather than naming a token.

func (Hue) String

func (h Hue) String() string

String names the hue by its meaning, not by its colour — the word is the vocabulary; the pastel is only how the word is spoken.

type Motion

type Motion struct {
	// Name is the motion's stable name; 18's table keys on it.
	Name string
	// Frames is the cycle in draw order. Empty means the motion has no frames
	// of ours — see [CaretMotion].
	Frames []string
	// Period is one full cycle. Zero means we do not drive it.
	Period time.Duration
	// Ease is the dwell bend in [0,1); 0 is a flat tick.
	Ease float64
	// Legal is where this motion may appear, in one sentence. A motion with no
	// legal surface is a motion that should not exist.
	Legal string
}

Motion is one row of the keyframe table: a name, the frames it cycles, how long one full cycle takes, how its dwell is eased, and the one sentence saying where it is legal.

func Motions

func Motions() []Motion

Motions returns the whole motion vocabulary, in 11's order. It is the code form of 18's keyframe table: motion_test.go walks it to hold every frame set width-stable and every period inside the calm band, and a `?` help surface or a motion-preview screen can walk the same rows.

A fourth entry here is a change to 11 and needs the law amended first.

type Pairing

type Pairing struct {
	Fg          Token
	FgFocus     Focus
	Ground      Token
	GroundFocus Focus
}

Pairing is one legal (foreground, ground) combination — a combination the surface is allowed to draw, and therefore one the contrast gate must cover.

func Pairings

func Pairings() []Pairing

Pairings enumerates every legal combination, in a stable order. The contrast test walks this list; a pairing missing from it is a pairing the surface may not draw, not a pairing that escaped review.

func (Pairing) Contrast

func (p Pairing) Contrast() float64

Contrast returns the contrast ratio a pairing actually achieves.

func (Pairing) Min

func (p Pairing) Min() float64

Min returns the ratio this pairing must achieve to ship.

type Profile

type Profile uint8

Profile is the color vocabulary a terminal actually has, ordered by capability so `p >= ANSI256` is a meaningful question.

const (
	// NoColor emits no SGR color at all: NO_COLOR, TERM=dumb, or a pipe. The
	// surface must remain fully legible here — every state that color carries
	// also has a glyph (5.17), which is why this profile is a degradation and
	// not a failure.
	NoColor Profile = iota
	// ANSI16 is the classic sixteen. Our pastels land in the bright family and
	// the terminal's own theme supplies the actual shade, so identity hues
	// collapse (see [Profile.IdentityDistinct]).
	ANSI16
	// ANSI256 is the xterm cube. Every token maps to a distinct index, and the
	// mapping deliberately avoids indices 0-15 because those are whatever the
	// user's theme says they are.
	ANSI256
	// TrueColor is 24-bit direct color: the palette exactly as authored.
	TrueColor
)

func DetectProfile

func DetectProfile(env Env) Profile

DetectProfile decides a terminal's color vocabulary from the STANDARD ecosystem variables only — NO_COLOR, TERM, COLORTERM, TMUX, TERM_PROGRAM. This package deliberately mints no environment pin of its own: an explicit override arrives as a Profile value through ParseProfile, so there is one door for capability and it is the shell's to open.

The governing rule from 10.1.2 is that COLORTERM is never trusted on its own: inside a multiplexer it is the multiplexer's claim about itself, not the outer terminal's capability, and tmux's own documentation is explicit that it forwards what it is told.

The ladder, highest precedence first:

  1. NO_COLOR (any non-empty value) — the cross-tool convention; honored unconditionally.
  2. TERM unset or "dumb" — no color.
  3. TERM naming a direct-color terminfo entry ("*-direct", "*-truecolor"). This is the one truecolor claim we accept without corroboration, because it is a claim about a terminfo database entry that exists on this machine, not a string a program can wish into the environment.
  4. Inside tmux or screen (TMUX set, or TERM prefixed "tmux"/"screen"): capped at 256 regardless of COLORTERM. A multiplexer that really does pass RGB through is expected to advertise it via a *-direct TERM, which rule 3 already caught.
  5. TERM_PROGRAM=Apple_Terminal — capped at 256. Terminal.app is the standard example of an emulator whose environment can end up carrying a truecolor claim it cannot honor.
  6. COLORTERM in {truecolor, 24bit} — truecolor.
  7. TERM containing "256" — 256 colors.
  8. Anything else with a TERM — 16 colors.

func ParseProfile

func ParseProfile(s string) (Profile, bool)

ParseProfile parses a profile by name ("none", "16", "256", "truecolor", plus the obvious synonyms), reporting whether the spelling was recognized.

It exists so an explicit override has a door WITHOUT this package minting an environment pin of its own. Colour capability is a decided value that the shell passes down — from a settings row when Wave 4 adds one, from a flag, or from DetectProfile — and an unrecognized spelling reports false rather than guessing, because an override that silently did something else would be the affordance lying about what it accepted (5.20).

func (Profile) BandTintDistinct

func (p Profile) BandTintDistinct() bool

BandTintDistinct reports whether the identity TINT on the selection band survives this profile. It is a separate question from [IdentityDistinct] and the answer is different: the tint is 8% of a hue mixed into a near-black ground (5.16 wants it "answered peripherally, never noticed"), and no 256-palette entry is that close to another. All eight tinted bands resolve to the same grey below truecolor.

A shell that asks and gets false should draw the plain Band rather than BandFor: the two produce identical bytes there, so the difference is only wasted work — but a shell that believed the tint was showing would also believe the room was announced, and it would not be.

func (Profile) IdentityDistinct

func (p Profile) IdentityDistinct() bool

IdentityDistinct reports whether the identity wheel survives this profile. At 16 colors eight identity hues collapse onto six chromatic slots, so the 5.16 promise — no two adjacent rail cards share an identity color — cannot be kept. A shell that asks this and gets false must stop drawing identity accents entirely rather than draw two neighbours the same color: a lying identity is worse than no identity (5.20, the affordance never lies).

func (Profile) SelectionStyle

func (p Profile) SelectionStyle() SelectionStyle

SelectionStyle returns the selection idiom available under this profile.

func (Profile) SheetGround

func (p Profile) SheetGround() bool

SheetGround reports whether a floating dialog may paint Sheet as its own ground under this profile. It is the same question SelectionStyle answers about the band, and it has the same answer for the same reason: below 256 colours there is no raised background this palette owns. At ANSI16 the only candidate is bright black — a different shade in every theme, and the one the chrome tier already lives in — so a sheet painted there would either vanish into the text or into the terminal's own background, depending on a setting we do not control. At NoColor there are no bytes to spend at all.

The dialog does not lose its boundary when this is false: 12.11's boundary is a one-cell margin ruled top and bottom, and the rule is a CHARACTER. The ground is the enhancement; the hairline is the floor.

func (Profile) String

func (p Profile) String() string

String names the profile. These are also the spellings ParseProfile accepts, so a settings row and a log line use one vocabulary.

type SelectionStyle

type SelectionStyle uint8

SelectionStyle says how selection must be drawn under this profile, and it is the ONE place that question is answered — a renderer asks here and never decides for itself, so the three answers cannot drift apart.

5.16 wants a background band. At 16 colors the only available "raised background" is bright black, which is a different shade in every theme and can land on top of the text tier; reverse video is the honest fallback there, because it is defined relative to whatever the terminal's own foreground and background are. At NoColor there is no SGR to spend at all — the profile is NO_COLOR, TERM=dumb, or a pipe — and a selection drawn in bytes that must not be written is a selection nobody can see.

So the degradation ladder ends where the ladder for every other state ends: "every state that color carries also has a glyph (5.17), which is why this profile is a degradation and not a failure" (see NoColor). Under SelectionMarker the cursor is carried by a CHARACTER — the ▎ accent rail (5.21: "structure without boxes"), in the gutter the map renderings already reserve. That is not a new idiom either: it is exactly what an unfocused pane already draws, because Legal forbids a dimmed foreground on a raised band, so the marker path was built and tested before this profile needed it.

const (
	SelectionBand    SelectionStyle = iota // draw [Band] / [BandFor] as a background
	SelectionReverse                       // SGR 7; no band token is used
	// SelectionMarker draws no ground at all: the row is marked with
	// [GlyphAccentRail] in the gutter. It is the only answer that costs zero
	// escape bytes, which is what makes it the right one for a profile defined
	// by having none to spend.
	SelectionMarker
)

type State

type State uint8

State is the liveness axis of 8.1.6: accent = live, plain = settled, dim = chrome.

const (
	// StateSettled is plain text: the row is done moving. Zero value, so a
	// bare struct renders settled — the safe default, because a row wrongly
	// drawn settled is quiet, and a row wrongly drawn live is a lie about
	// liveness (8.1.6).
	StateSettled State = iota
	// StateLive is the accent: something is happening on this row now.
	StateLive
	// StateChrome is dim: separators, meta, fold lines, hints.
	StateChrome
)

func (State) String

func (s State) String() string

String names the state.

type Styler

type Styler struct {
	// contains filtered or unexported fields
}

Styler paints text with this package's palette for one terminal profile and one pane focus. It is immutable after construction and safe to share across goroutines: every field is read-only and every escape sequence it writes was computed once, at package initialization, by the palette table.

A Styler is a value worth caching per pane, not per frame — construct one when the profile or the pane's focus changes, and hold it.

func NewStyler

func NewStyler(p Profile, f Focus) *Styler

NewStyler returns a Styler for a terminal profile and a pane focus, on the plain glyph tier. Its signature is unchanged and always will be: every construction site written before the tier existed keeps compiling and keeps rendering exactly what it rendered, which is the whole contract of an enhancement ladder.

An out-of-range profile or focus is clamped rather than panicking. This is the one place in the package that forgives bad input, and the reason is that its argument usually comes from a terminal probe: a render must not die because an emulator lied about itself, and the honest degradation for "capability unknown" is no colour.

func NewStylerIn

func NewStylerIn(p Profile, f Focus, g GlyphSet) *Styler

NewStylerIn is NewStyler with the third axis: the glyph repertoire tier (12.7). It composes with profile and focus and changes neither — the tier decides which character lands in a cell, never how many cells a line takes, which token tints it, or where a segment sits.

func (*Styler) Fg

func (s *Styler) Fg(t Token) string

Fg is the foreground sequence this Styler paints a token with, and it is THE ONE DOOR: every painter here and in internal/tui2/prose asks it rather than reaching past to Token.Fg, so an override stated once is honoured everywhere a row is assembled rather than on whichever paths somebody remembered.

A nil Styler and an out-of-range token both answer "", which is the same nothing NoColor answers: there is no colour to write, and no reason to panic on the way to writing none.

func (*Styler) Focus

func (s *Styler) Focus() Focus

Focus reports the pane focus this Styler paints for.

func (*Styler) Glyph

func (s *Styler) Glyph(id GlyphID) string

Glyph is the EXPLICIT door to the vocabulary: it resolves a slot in this Styler's tier. Every consumer can use it, and the six slots whose plain side is ASCII — "?" needs-human, "=" paused, "$" spend, "/" folder and the diff signs — have no other door, because those characters are things a user types and the automatic path must never rewrite one (12.7 D.3).

A nil Styler resolves the plain glyph rather than panicking, and that forgiveness is deliberate where the painting paths' is not. A nil Styler is a real state in this tree — a block built before a profile was chosen holds one — and a consumer adopting the tier replaces a package-level CONSTANT with this call. If the call could panic where the constant could not, adoption would be a one-token edit that changes when a renderer crashes, and every consumer would have to grow a nil check for a lookup that reads no colour and makes no decision. Painting is different: it must produce escape bytes, and there is no honest answer to "which colour" without a profile.

func (*Styler) GlyphSet

func (s *Styler) GlyphSet() GlyphSet

GlyphSet reports the glyph repertoire tier this Styler draws in. A nil Styler reports Plain — see Styler.Glyph for why that is safe here and not for painting.

func (*Styler) PaintOn

func (s *Styler) PaintOn(text string, fg, bg Token) string

PaintOn draws text in fg over the background bg. It is the selection band of 5.16 ("selection is a background band, not a foreground colour"): the row keeps its tier colour and gains a raised ground.

Under a profile whose Profile.SelectionStyle is SelectionReverse there is no trustworthy raised background, so the band becomes SGR 7 — the honest fallback, defined relative to whatever the terminal's own colours are. Legal governs which pairs may be drawn at all, and the contrast gate has measured every one of them.

Under SelectionMarker it returns the text unpainted, and that is the whole of what this function may do: painting must not change printable width, and a marker is a CELL. A renderer whose profile answers SelectionMarker therefore has to draw GlyphAccentRail in its gutter — ask Profile.SelectionStyle before drawing a row, not this function after.

func (*Styler) PaintRowOn

func (s *Styler) PaintRowOn(row string, ground Token) string

PaintRowOn lays an ALREADY-PAINTED row onto a raised ground.

It is the door a card needs and Styler.PaintOn cannot be: PaintOn takes one span, and a card's row is a dozen spans a dozen renderers painted — a header grammar, a markdown pass, a fold hint — none of which knows it is standing on a plane. Asking each of them to thread a ground through would be threading a background into every renderer in the tree so that one block kind could have a floor.

IT WORKS BECAUSE THE PACKAGE'S OWN RESET IS FOREGROUND-ONLY. [Styler.paint] closes a span with SGR 39, which clears the colour it set and nothing else, so a background armed before the row survives every span inside it. There is exactly one sequence in this package that does clear a background — the [sgrResetAll] PaintOn writes — and it is re-armed here rather than left to punch a hole in the plane: a code span inside a card's body is a real case, and a card whose ground stopped halfway along a row would be a rendering bug nobody could see the cause of. One known sequence, one repair, both stated.

Printable width is untouched, as it is for every other painter here. An empty row is returned as it came: a background around nothing is bytes for no reason, and a zero-width painted string makes every "is this row blank" check downstream answer wrong.

func (*Styler) PaintToken

func (s *Styler) PaintToken(text string, t Token) string

PaintToken is the direct door for a renderer that already knows its token — the grey-ramp tiers, a band, a promoted cell — and does not need the hue and state axes to resolve one for it.

func (*Styler) Profile

func (s *Styler) Profile() Profile

Profile reports the terminal profile this Styler paints for.

func (*Styler) WithBodyInk

func (s *Styler) WithBodyInk(c Color) *Styler

WithBodyInk returns a Styler that paints the body tier — TextPrimary — in a STATED colour rather than in this palette's own.

── WHY THIS SEAM EXISTS: ONE SURFACE, ONE BODY WHITE ──

internal/tui3 authors its own quieter palette and then renders a model's markdown through internal/tui2/prose, which is the only markdown renderer in the tree and deliberately resolves every colour on the row from this package (prose's own package comment says why it will not seat a second colour authority). The consequence was two whites on one screen: prose painted the reply's body at TextPrimary #E6E6F0 while everything tui3 drew around it wore tui3's own, dimmer ink. The brightest thing on the surface was therefore the thing a person reads most — about 14:1 against a dark terminal where the accent beside it sat at 9.6:1 — which is glare, and halation that makes the strokes read heavier than they are.

The fix is not a second renderer and not a copy of the ramp. It is that the COLOUR AUTHORITY a caller hands prose may be asked to say the body tier in the caller's own voice. One Styler, one answer to "which white", and the ladder's shape untouched: an h1 is still the top of the grey ramp and still bold, an h3 still steps down through Demote, a fenced block still lands on the code ramp and an inline span still stands on the Sheet. What moves is the VALUE at the top of the ramp, so everything standing on it moves together — which is the point, because a tier the body alone left behind would simply relocate the glare onto the headings.

A Styler built by NewStyler or NewStylerIn carries no override, so every construction site in this tree keeps rendering exactly the bytes it rendered.

── WHAT IT COSTS PER PROFILE ──

The override is a COLOUR, so it exists only where the profile has colours to spend. TrueColor takes it exactly. ANSI256 takes its nearest cube-or-grey neighbour, computed here by the same [nearest256] this package's own table is built with — a caller whose palette rounded the same hex to a different index would be two whites again on the majority profile, and one resolver is what stops that. ANSI16 and NoColor take NOTHING and fall back to the token: the sixteen are the user's own theme, so there is no honest form for an authored hex there (the wall Token.UnderlineColor meets from the other side), and a profile told to write no SGR at all writes none.

The DIMMED variant is derived rather than asked for, by the one rule the whole palette is dimmed with (DimTowardGround). A caller states the colour it reads at; an unfocused pane then recedes by the same law every other token obeys, instead of staying at full strength because nobody thought about it.

CONTRAST IS THE CALLER'S TO JUSTIFY. contrast_test.go gates Pairings, and a colour that is not in the table is not in that walk — so the caller who states one owes it the check its own palette owes. internal/tui3 pins its body ink with a contrast law of its own for exactly this reason.

func (*Styler) WithFocus

func (s *Styler) WithFocus(f Focus) *Styler

WithFocus returns a Styler identical to s but painting at the given focus. Dimming is a property of the pane (8.3), so a compositor that has just lost focus swaps one small value rather than re-resolving every row.

It COPIES rather than reconstructing, which it did not have to do before Styler.WithBodyInk existed: a reconstruction goes through NewStylerIn, and NewStylerIn knows nothing about an override, so a pane that lost focus would silently get the palette's own body tier back. Copying is exactly equivalent for everything that used to be re-derived — neither `enabled` nor `upgrading` depends on the focus.

func (*Styler) WithGlyphSet

func (s *Styler) WithGlyphSet(g GlyphSet) *Styler

WithGlyphSet returns a Styler identical to s but drawing in the given tier. It mirrors Styler.WithFocus — copy included, for the same reason — and it is what a settings sheet's live preview renders its two sample lines through.

type Token

type Token uint8

Token names every color the surface may draw. It is a dense small integer so tables can be indexed by it and a Token can live in a struct field for free.

const (
	TextPrimary   Token = iota // tier 1: speech, titles
	TextSecondary              // tier 2: status lines, receipts
	TextTertiary               // tier 3: telemetry (and interactive chips at rest)

	Amber // needs a human: question badges, waiting states, ctx meter near limit
	Cyan  // alive: working glyphs, stream caret, thinking pulse
	Green // money + success: cost figures, settled ✓
	Coral // broken: failures, cancels

	Identity0
	Identity1
	Identity2
	Identity3
	Identity4
	Identity5
	Identity6
	Identity7

	Ground // the default dark ground
	Band   // selection band background (5.16: selection is a band, not a color)
	// Sheet is a floating dialog's OWN ground — the palette, the `?` capability
	// surface, the settings sheet, the consent dialog, and the one-cell margin
	// 12.11 rules top and bottom.
	//
	// It exists because 12.11's boundary was drawn and never painted, and an
	// unpainted panel has no ground: it inherits whatever the terminal's
	// default background is, which is the same nothing the transcript behind it
	// inherits. Two rooms with the same floor read as one room, which is 12.13's
	// wall finding arriving on the other axis — there the fix was a column of
	// ground, here it is a PLANE of it.
	//
	// It is deliberately the smallest step that survives every profile rather
	// than the largest step that looks impressive: 5.13 spends the structure
	// budget on whitespace and hairlines, and a dialog that announced itself
	// with a loud slab would be the box 5.21 refuses wearing a background.
	// [Profile.SheetGround] is the one place that says where it may be drawn at
	// all — at 16 colours the only raised background is bright black, which is
	// the user's theme's to define, so the sheet keeps its hairline boundary
	// and paints no ground there.
	Sheet

	BandIdentity0 // selection band tinted with identity 0..7, used inside that
	BandIdentity1 // task's scope so "which room am I in" is answered
	BandIdentity2 // peripherally
	BandIdentity3
	BandIdentity4
	BandIdentity5
	BandIdentity6
	BandIdentity7

	// HugGroundBar and HugGroundInput are the composer hug's two grounds: the
	// row that names where you are, and the row you type into, one shade
	// lighter (see [HugBarTowardBand]).
	//
	// They are two tokens rather than one because the hug's depth IS the step
	// between them — a single ground would be the slab this pair replaced. They
	// are below the [Sheet] rather than at it because permanent chrome may not
	// announce itself as loudly as a dialog that came and will go, and above the
	// [Ground] because a plane the transcript slides under has to be a different
	// plane at all. [Profile.SheetGround] gates them exactly as it gates the
	// sheet: at 16 colours and none, the hug paints no ground and the blank row
	// the region already leaves is the whole seam.
	HugGroundBar
	HugGroundInput

	// CardGroundWorking and CardGroundDelivered are the two grounds a task's
	// card in the conversation stands on: the commitment while the work runs,
	// and the delivery once it has come back (§4, §18.5).
	//
	// They are two tokens for the reason the hug's are: the STEP between them is
	// the information. A single card ground would say "this is a card" and leave
	// "is it finished" to a word. See [CardWorkingTowardBand] for the numbers and
	// for why the delivered rung stops short of the band's floor.
	//
	// [Profile.SheetGround] gates both exactly as it gates the sheet and the hug:
	// at 16 colours and none there is no honest raised background, so a card
	// paints no ground and is carried by its edge, its glyph and its spacing —
	// which is the half of the treatment §4 actually specifies.
	CardGroundWorking
	CardGroundDelivered
)

The token inventory. Order is load-bearing only for Identity0..Identity7 and BandIdentity0..BandIdentity7, which are contiguous so a wheel index can be added to the first member.

func All

func All() []Token

All returns every token in inventory order. Tests, the golden harness, and a palette-preview screen all want to walk the whole set.

func BandFor

func BandFor(identity Token) Token

BandFor returns the identity-tinted selection band for an identity token. Passing anything that is not an identity token returns the plain Band — the home scope has no identity, and its selection is untinted.

func CutToken

func CutToken(c CutKind) Token

CutToken is the colour of a cut mark, and the distinction it draws is the honest one:

  • A LENGTH CAP or a STREAM DROP is coral. Something broke; the answer on screen is not the answer the model meant to give, and 12.5's whole finding is that an unmarked half-artifact is a lie of omission.
  • An INTERRUPT is chrome. The user did it on purpose, and nothing is broken. Painting the user's own esc key coral would be the surface scolding someone for using it.

Amber is deliberately not offered: amber means a human is needed (5.16), and a cut turn is not a question. The repair doctrine (12.5.3) says the head re-produces and says so; it does not stand there asking.

func Demote

func Demote(t Token) Token

Demote is Promote's inverse for the grey ramp: primary → secondary → tertiary, tertiary stays put, hues unchanged. It exists for the one move 5.15 needs — pushing chrome down a tier while a scope is entered, so the room the user is in outranks the room they came from — and it is deliberately NOT how an unfocused pane recedes. That is FocusDimmed, which dims every tier by the same rule instead of collapsing the hierarchy into it.

func ElapsedToken

func ElapsedToken(elapsed, estimate time.Duration) Token

ElapsedToken implements "elapsed that ages" (5.21): dim while the work is inside its estimate, one tier brighter once it is past. It brightens; it never turns amber — amber means a human is needed (5.16), and a slow worker is not asking for anything. With no estimate the reading stays chrome.

func Identity

func Identity(i int) Token

Identity returns the identity token for a wheel index, wrapping. It keeps the eight-pastel band total when a caller's index falls outside one turn.

func Promote

func Promote(t Token) Token

Promote brightens a grey-ramp token one tier: tertiary → secondary → primary, and primary stays put. Hues are already the loudest thing on a row and are returned unchanged — brightening a pastel would break the contrast pairs the gate tested (5.16), and a louder amber does not mean a more urgent question.

Two callers, and both are the same idea — a thing that normally recedes has briefly earned the eye:

  • 5.22: a chrome control that is also a button brightens one tier on focus, which is why no interactive control lives permanently at the chrome contrast gate.
  • 5.21's "elapsed that ages": see ElapsedToken.

func ResolveToken

func ResolveToken(h Hue, s State) Token

ResolveToken composes the hue and state axes into the one token a cell draws with. It is the ONLY place the composition rule lives, and the rule is three lines long on purpose:

  1. A HUE ALWAYS WINS. A cell that means something keeps meaning it after the row settles — 5.16's green settled ✓ and coral ✕ are settled cells that are still coloured. 8.1.6's "completion settles accent → plain text" governs the row's PROSE, which carries no hue; the glyph beside it does.
  2. With no hue, the state axis picks the tier: live is cyan, because cyan is the word for alive (5.16) and "accent = live" has to resolve to some accent; settled is the primary grey; chrome is the tertiary grey.
  3. The SECONDARY tier is never reached from here. A status line is secondary because of WHAT IT IS (5.13's type hierarchy), not because of how live it is — a renderer names TextSecondary directly. Deriving it from the state axis would make two different questions share one answer.

It is a pure switch over two small enums: no allocation, no table lookup, and nothing for the compiler to fail to inline.

func TokenForANSI16

func TokenForANSI16(index int) Token

TokenForANSI16 answers the other direction: which of our tokens is the nearest reading of a raw ANSI-16 index. It is what a renderer uses when it wants to draw remapped tool output in OUR colors on a truecolor terminal — the fidelity path the table above cannot take. Indices with no semantic word in the five-hue vocabulary resolve to the grey ramp rather than borrowing an identity hue, because identity means "which task", and a tool's blue does not.

func (Token) Bg

func (t Token) Bg(p Profile, f Focus) string

Bg is Token.Fg for the background. Only surface tokens are normally drawn this way, but the table is complete so a preview screen can show any token as a swatch.

func (Token) Class

func (t Token) Class() Class

Class returns the contrast contract this token signs.

func (Token) Color

func (t Token) Color(f Focus) Color

Color returns the token's value in a focus state.

func (Token) Fg

func (t Token) Fg(p Profile, f Focus) string

Fg returns the precomputed SGR sequence that sets this token as the foreground under a profile and focus. It is a constant string built once at package initialization: a render appends it, never formats it. NoColor returns "".

func (Token) Hex

func (t Token) Hex(f Focus) string

Hex returns the token's value as "#RRGGBB" — the form lipgloss and the golden-test harness read.

func (Token) Index

func (t Token) Index(p Profile, f Focus) uint8

Index returns the palette index this token resolves to under a profile: an ANSI-16 index for ANSI16, an xterm-256 index for ANSI256. It is meaningless for TrueColor and NoColor, which return 0 — callers wanting exact color ask Token.Color.

func (Token) IsSurface

func (t Token) IsSurface() bool

IsSurface reports whether the token is a background rather than a foreground. Surfaces have no contrast gate of their own.

func (Token) String

func (t Token) String() string

String is the token's stable name ("amber", "text.tertiary", "band.identity.3"). Golden tests key on these, so they are part of the API.

func (Token) UnderlineColor

func (t Token) UnderlineColor(p Profile, f Focus) string

UnderlineColor is SGR 58: the underline's own colour, so a bright word can carry a coloured rule without the word itself changing tier.

It returns "" where the profile has no honest form for it — NoColor, and ANSI16, where the only 58 form is the 256-colour one and a 16-colour terminal's palette is the user's theme to define. Both fall back to a plain [Underline], which is the widely-supported half of the pair; a caller writes the two together and gets whatever the terminal can carry.

Jump to

Keyboard shortcuts

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