How it works
Caret has four layers. Top-down, each layer reads only from the one below it. There is no central state, no config, no provider tree — a change in any leaf is a one-file edit.
The four layers
| Layer | What it does | Lives in |
|---|---|---|
| Components | React-for-the-terminal primitives — prompts, spinners, tables, errors. One file each. | registry/components/ |
| Tokens | Colors, motion, symbols, spacing, typography. The visual contract. | registry/tokens/ |
| Capability | Detects TTY, NO_COLOR, narrow terminals, reduced motion. Components consult it before painting. | registry/lib/capability.ts |
| Spec | Per-component markdown describing anatomy, API, keyboard shortcuts. Source of truth for AI assistants and ports. | specs/ |
Components
Each component is a single file rendered with Ink (React for terminal). Output is inline — scrollback-friendly, pipe-friendly, log-friendly. Components emit foreground colors and attributes, not backgrounds. They never assume a TTY; the capability layer tells them when they have one.
A component's API is a single options object. No positional args, no method chains, no fluent builders. prompt.text({ label, validate }) is the same shape as error(title, { body, hint, see }) — and AI assistants treat them the same way.
Tokens
Tokens are the contract between Caret and the visual layer. Components never write hex codes or magic numbers — they read tokens through the active theme. Re-skin the entire system with a single setTheme() call, or pass a theme override per-component for a one-off.
Brand color is one fixed truecolor; semantic colors are emitted as ANSI names (green, red) so they harmonize with the user's terminal theme. See Tokens for the full table.
Capability
lib/capability.ts is one synchronous read — TTY, NO_COLOR, terminal width, dumb terminal, reduced motion. Components call it once at render time and choose the right path: truecolor → 256 → ANSI 16 → plain. You don't enable the fallback chain; you'd have to go out of your way to break it.
Spec
Every component has a specs/<name>.md file — anatomy diagrams, API tables, keyboard shortcuts, accessibility notes. Specs are language-agnostic. Someone porting Caret to Go, Rust, or Python implements from the spec, not from TypeScript source.
Specs also bind AI assistants. The caret.md file at your project root tells Claude / Cursor / Copilot to consult the spec before guessing at behaviour.
Lifecycle
npx caret-cli initscaffolds a project (one-time).caret add <component>copies that component's files intocaret/in your repo (per component, per decision).- You import from
./caret, run your CLI, and the components render via Ink. - Updates? Re-run
caret addwith the same name to pull the latest version. Diff the changes, accept what you want.
What Caret is not
- Not a TUI framework. Caret is for CLIs that print and exit, not fullscreen apps like
k9sorlazygit. For those, reach for Ink, Textual, or Ratatui directly. - Not a runtime dependency. Components live in your repo, not in
node_modules/@caret/.... - Not a terminal emulator. Caret runs inside iTerm, Alacritty, Wezterm, Ghostty — whatever you use.