/** * The Bytefly terminal design tokens: semantic roles mapped onto Pi's theme * colors. * * A component names a role (`title`, `meta`, `bad`) and the active theme * supplies the color, so switching themes re-skins every surface without a * component changing. The mapping is deliberately indirect: Pi's token set is * closed, so the kit only ever references it, never a literal color — and this * table is the one place it does, tool vocabulary included, so a remap here * reaches every surface the kit and the renderers draw. * * Value-only: one frozen lookup table and pure helpers. * * Two vocabularies live here and they do not mix: `ok`/`warn`/`bad` are verdicts — * they say how something *is* — and `hue1`…`hue5` are categories, which say only * "this is not those". A surface that shows hues keeps its verdicts to the places * that earn one, because a reader cannot tell a borrowed green from a judgement. */ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent"; /** A semantic role a surface can paint. */ export type TuiRole = /** The brand highlight: selection, focus, a model name. */ | "accent" /** A screen, section, or panel title. */ | "title" /** Primary body text. */ | "label" /** Secondary text: counts, paths, summaries. */ | "meta" /** Tertiary text and de-emphasis. */ | "faint" /** A divider line. */ | "rule" /** A heavier divider. */ | "ruleStrong" /** Success. */ | "ok" /** Warning. */ | "warn" /** Error. */ | "bad" /** Neutral notice. */ | "info" /** * A category hue: identity, never a verdict. * * Blue, purple, pink, green, and amber are the shipped palette's five colourful * tokens, so a categorical surface reads as a chart. Green and amber are the same * tokens `ok` and `warn` resolve to — that is the cost of Pi's closed token set — * which is why a hue never appears in a slot that also carries a judgement. */ | "hue1" | "hue2" | "hue3" | "hue4" | "hue5" /** A tool call's own name: the transcript block's only accent. */ | "toolTitle" /** A tool result's body text. */ | "toolOutput"; /** Role -> theme color. The single place the kit names a Pi token. */ export const TUI_TOKENS: Readonly> = { accent: "accent", title: "accent", label: "text", meta: "muted", faint: "dim", rule: "borderMuted", ruleStrong: "border", ok: "success", warn: "warning", bad: "error", info: "muted", hue1: "accent", hue2: "mdListBullet", hue3: "mdCode", hue4: "success", hue5: "warning", toolTitle: "toolTitle", toolOutput: "toolOutput", }; /** Paint `text` in a role's color. */ export function paint(theme: Theme, role: TuiRole, text: string): string { return theme.fg(TUI_TOKENS[role], text); } /** * The sentinel a derivation wraps: a painting wrapper is split at it, so a * frame reads the theme's own escapes rather than assuming a shape. */ const MARKER = "\u0000"; /** The escapes a painting wrapper puts before `text`, and the ones after it. */ function wrapEscapes(wrapped: string): { open: string; close: string } { const at = wrapped.indexOf(MARKER); return { open: wrapped.slice(0, at), close: wrapped.slice(at + MARKER.length) }; } /** * The opening sequence for a role, without its reset. A frame that must repaint * a run in place — mid-line, where the surrounding text already carries the * closing reset — opens with this instead of wrapping a whole string. */ export function paintPrefix(theme: Theme, role: TuiRole): string { return wrapEscapes(paint(theme, role, MARKER)).open; } /** * The sequence that ends the theme's bold run, for a frame that must stop the * style mid-line: a call line keeps its tool name bold and reads the command * after it plain. Derived like {@link paintPrefix}, so it closes nothing in a * theme that does not bold. */ export function boldClose(theme: Theme): string { return wrapEscapes(theme.bold(MARKER)).close; }