Themes
The console ships four built-in colour schemes. Switch between them in
/settings— there are no theme files to write.
The console's theme system is code-defined, not file-defined. Each scheme is built at module load from a small raw colour ramp, which is expanded into a closed set of semantic tokens and then projected onto the framework's colour adapter. There is no JSON theme format, no ~/.indusagi/.../themes/ directory, and no --theme flag — a scheme is selected by name and resolved against the shipped table.
The theme engine lives in src/console/theme. The public barrel is src/console/theme/index.ts.
Table of Contents
- The four schemes
- Selecting a scheme
- The pipeline
- Palettes
- Semantic tokens
- Framework colour keys
- Resolving a scheme
- Color-blind schemes
The four schemes
Every scheme name is a ThemeScheme (src/console/contract.ts):
| Scheme | Label | For |
|---|---|---|
midnight |
Midnight | dark terminals (the default) |
daylight |
Daylight | light terminals |
midnight-cb |
Midnight (color-blind) | deuteran-safe, dark |
daylight-cb |
Daylight (color-blind) | deuteran-safe, light |
DEFAULT_SCHEME is midnight. These four are the complete shipped set; they are listed in this order by both the theme picker (THEME_CHOICES in src/console/overlays/pickers.tsx) and the THEME_SCHEMES table.
Selecting a scheme
The scheme is stored under the colourScheme preference. Its default value is the literal "default", which is not a known scheme name and therefore resolves to midnight (see Resolving a scheme).
Set it from settings.json:
{
"colourScheme": "daylight"
}
Or change it interactively in the Settings overlay (/settings → "Colour scheme"). Writing the value there persists it immediately and re-themes the live surface.
The console also defines a dedicated colour-scheme picker (the theme modal, rendered by ThemePicker in src/console/overlays/pickers.tsx via the framework ThemeDialog). In this build the picker has no opener wired — no slash command and no keybinding raises the theme modal — so the Settings overlay above is the reachable way to switch schemes. When the theme modal is raised, its semantics are:
- Enter — commit: dispatch
scheme:setto re-theme the live surface and persistcolourScheme. - Esc / cancel — close the picker, reverting to the scheme that was on screen when it opened.
The picker captures the opening scheme as a revert target, but it does not wire the framework dialog's optional onHighlight callback, so moving the highlight does not live-preview a scheme — nothing changes until Enter commits.
The pipeline
A scheme is assembled once at module load (src/console/theme/resolve.ts):
palette ──deriveTokens──▶ tokens ──themeAdapter──▶ framework adapter
└──────────────────────────┴───────────────────────────┘
ConsoleTheme
A ConsoleTheme carries the scheme name, the raw palette, the derived tokens, and the framework adapter the components render against. The heavy work (building the chalk-backed adapter) happens at load time, so the render path only ever reads an already-built theme.
Palettes
A ThemePalette (src/console/theme/palette.ts) is the raw nine-stop ramp a scheme is built from: three accent hues (a cool primary, a warm secondary, a muted-support tertiary), a three-stop neutral text gradient (ink/body/muted), and three status hues (affirm/caution/alarm).
export interface ThemePalette {
primary: string; // cool accent (focus, selection, headings)
secondary: string; // warm accent (composer tint, card gutters)
tertiary: string; // muted-support accent (borders, info, types)
ink: string; // high-contrast text on an accent
body: string; // default answer foreground
muted: string; // timestamps, hints, metadata
affirm: string; // success
caution: string; // warning
alarm: string; // error / fault
}
The two base ramps are MIDNIGHT_PALETTE and DAYLIGHT_PALETTE; PALETTES maps every ThemeScheme to its ramp. Every colour the console renders is computed from these stops — no module downstream of the palette writes a literal hex.
Semantic tokens
deriveTokens (src/console/theme/tokens.ts) expands a palette into the ThemeTokens the console body actually names. The console never names a hex or a palette stop; it names a role. The mapping is pure and total — every token key is assigned exactly once.
Core roles:
| Token | ← palette stop | Purpose |
|---|---|---|
signal |
primary |
dominant accent (focus, selection) |
frame |
tertiary |
default structural borders |
quietFrame |
muted |
de-emphasised separators |
promptSurface |
secondary |
composer's active tint |
cardAccent |
secondary |
row gutters and headers |
bodyText |
body |
default answer foreground |
mutedText |
muted |
timestamps, hints, metadata |
inkText |
ink |
high-contrast text on an accent |
notice |
tertiary |
informational tone |
affirm |
affirm |
success tone |
caution |
caution |
warning tone |
alarm |
alarm |
error / fault tone |
pending |
primary |
busy / in-flight tone |
Rich-render roles (markdown / diff / syntax) are derived from the same nine stops so styled transcripts, colored diffs, and fenced code recolour with the scheme:
| Token | ← derived from | Purpose |
|---|---|---|
codeInline |
primary |
inline code accent |
heading |
primary |
heading accent |
blockquoteBar |
muted |
dim quote bar |
diffAddedBg |
affirm blended toward the bg |
added-line tint |
diffRemovedBg |
alarm blended toward the bg |
removed-line tint |
diffAddedText |
affirm |
added foreground / + |
diffRemovedText |
alarm |
removed foreground / - |
synKeyword |
primary |
keywords |
synString |
affirm |
strings |
synNumber |
caution |
numbers |
synComment |
muted |
comments |
synType |
tertiary |
types / classes |
The two diff backgrounds are computed by blending the status hue ~22% over the scheme's implied terminal background (toward black on a dark scheme, toward white on a light one), so every scheme — including the color-blind variants — gets a legible +/- tint that tracks its own success/alarm hue.
Framework colour keys
The framework's InkThemeAdapter looks colours up by its own key vocabulary. frameworkColors (src/console/theme/adapter.ts) is the one place each framework key is filled from a console token, and themeAdapter hands the resulting record to createThemeAdapter from indusagi/react-ink:
| Framework key | ← console token |
|---|---|
text |
bodyText |
dim |
mutedText |
muted |
mutedText |
accent |
signal |
borderMuted |
quietFrame |
bashBorder |
frame |
userMessage |
promptSurface |
customMessage |
cardAccent |
success |
affirm |
warning |
caution |
error |
alarm |
info |
notice |
highlight |
inkText |
The markdown/diff/syntax role keys (codeInline, heading, blockquoteBar, diffAddedBg, diffRemovedBg, diffAddedText, diffRemovedText, synKeyword, synString, synNumber, synComment, synType) carry through under the same names so the framework's rich-render path resolves them straight from the derived tokens.
Resolving a scheme
resolveTheme (src/console/theme/resolve.ts) is the single sanctioned way to turn a scheme name into a ConsoleTheme:
import { resolveTheme, THEME_SCHEMES, isThemeScheme } from "src/console";
const theme = resolveTheme("daylight"); // → the daylight ConsoleTheme
const fallback = resolveTheme("nonsense"); // → the midnight (default) ConsoleTheme
An unrecognised or absent name falls back to DEFAULT_SCHEME rather than throwing, so a corrupt colourScheme preference never blanks the console. isThemeScheme(value) narrows an arbitrary string to a known scheme; THEME_SCHEMES is the picker-ordered list of every scheme.
Color-blind schemes
midnight-cb and daylight-cb clone their base ramps but re-derive the three status hues so a red-green color-blind user can tell success from failure without relying on the red-green axis. Success moves off green onto a vivid blue, the red alarm is deepened, and the amber warning is nudged in lightness — so the three status tones separate by lightness, not hue alone. The accent hues and neutral gradient carry over unchanged, so the overall look stays the parent scheme.
