All docs
Overview · How it works

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

LayerWhat it doesLives in
ComponentsReact-for-the-terminal primitives — prompts, spinners, tables, errors. One file each.registry/components/
TokensColors, motion, symbols, spacing, typography. The visual contract.registry/tokens/
CapabilityDetects TTY, NO_COLOR, narrow terminals, reduced motion. Components consult it before painting.registry/lib/capability.ts
SpecPer-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

  1. npx caret-cli init scaffolds a project (one-time).
  2. caret add <component> copies that component's files into caret/ in your repo (per component, per decision).
  3. You import from ./caret, run your CLI, and the components render via Ink.
  4. Updates? Re-run caret add with 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 k9s or lazygit. 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.