theme
Colours. Seven tokens, closed on purpose — every surface derives from these, which is what makes a theme portable.
interface ThemeApi {
// Register a theme. Throws if the name is taken.
create(name: string, tokens: ThemeTokens): Disposer
// Paint one and keep it (persisted). Throws on an unknown name.
set(name: string): void
// Every registered theme, built-ins first.
list(): readonly Theme[]
// The theme currently painted.
readonly active: Theme
}
type ThemeTokens = {
primary: ThemeColor // accent: selection, input rule, focused rows, links
background: ThemeColor // the terminal's ground
text: ThemeColor // ordinary foreground
dim: ThemeColor // paths, descriptions, timestamps
warn: ThemeColor // needs attention, nothing failed
error: ThemeColor // something failed
syntax: BundledSyntax | Record<string, unknown>
}
type Theme = ThemeTokens & { name: string }
type ThemeColor = ColorName | "transparent" | (string & {})
Before this existed the TUI had reached ~30 hard-coded colours — four greys nobody chose deliberately, five reds. A theme API with a field per call site would have preserved that forever.
Registering
theme.create("midnight", {
primary: "#7aa2f7",
background: "transparent",
text: "#c0caf5",
dim: "#565f89",
warn: "#e0af68",
error: "#f7768e",
syntax: "tokyo-night",
})
theme.set("midnight") // create does not paint
Colours take a name, hex, rgb()/rgba(), or transparent — which means "paint nothing
and let the real terminal show through", usually right for background.
syntax takes a bundled theme name or a TextMate object. Unlike colours that union is
closed: an unrecognised colour still renders, but an unrecognised syntax name loads
nothing, so it is always a typo.
Deriving from an existing theme
const base = theme.list().find(t => t.name === "gruvbox")
if (base) theme.create("gruvbox-dim", { ...base, dim: "#665c54" })
list() entries carry their full token set, not just the name — as does theme.active.
Live preview
palette.create("themes", {
key: "T",
list: () => theme.list().map(t => ({
id: t.name,
label: t.name,
preview: () => theme.set(t.name), // paints as the cursor moves
action: () => theme.set(t.name),
})),
})
That is what " does.
Shipping one
A theme is an ordinary extension — theme.create in its main.ts, published to the
registry, installed with axon install. It loads from
profile.config.ts like any other.