Capability detection
Every Caret component asks one question before painting: what does this terminal actually support? The answer comes from lib/capability.ts, a single synchronous read that every primitive consults. You don't enable any of this — you'd have to go out of your way to break it.
What it detects
| Signal | Source | Effect |
|---|---|---|
| isTTY | process.stdout.isTTY | Pipe-aware fallback — when piped, components emit plain text without escape codes |
| hasColor | NO_COLOR env var, FORCE_COLOR, CI heuristics | Strips chalk colors entirely when set |
| truecolor | COLORTERM=truecolor / TERM=*-256color | Picks 24-bit RGB vs 256-color vs ANSI 16 |
| unicode | LANG / LC_ALL contains UTF-8 | Falls glyphs back to ASCII (✓ → +, │ → |) when missing |
| columns | process.stdout.columns | Wraps tables, banners, paragraphs to fit narrow terminals |
| reducedMotion | CARET_REDUCED_MOTION, prefers-reduced-motion shim | Disables spinner / typewriter / reveal animations |
| dumb | TERM=dumb | Plain output, no escape codes, no animation |
Reading capability in your own code
If you're authoring a custom component or extending a Caret one, call capability() directly. It's cheap — synchronous, no cache invalidation needed.
import { capability } from './caret/lib/capability'
const cap = capability()
if (!cap.isTTY) {
// Output is being piped — emit plain JSON, not pretty terminal UI.
console.log(JSON.stringify(result))
return
}
if (cap.dumb || !cap.unicode) {
// Use ASCII fallbacks throughout this command.
}The color fallback chain
Components walk the same chain in this exact order — first match wins:
NO_COLORset? Plain text, no escape codes at all.- Not a TTY? Plain text — pipe-friendly.
- Truecolor terminal? Brand accent emitted as 24-bit RGB; semantic colors as ANSI named.
- 256-color terminal? Brand accent quantized to the closest 256-color slot.
- ANSI 16 terminal? Brand accent maps to the closest named ANSI color.
- Dumb terminal? Plain text, hierarchy expressed via symbols only.
Environment variables
| Variable | Effect |
|---|---|
| NO_COLOR=1 | Disable color in all Caret output (also strips third-party chalk via Caret's paint helper) |
| FORCE_COLOR=3 | Force truecolor even when stdout is not a TTY (CI snapshots) |
| CARET_REDUCED_MOTION=1 | Disable all motion regardless of OS preference |
| CARET_NO_NOTIFY=1 | Disable system notifications from spinner / prompts.notifyOnWait |
| TERM=dumb | Plain output, ASCII glyphs, no animation |
process.stdout.isTTY is false. Pipe a Caret CLI through grep, jq, or save it to a file — scrollback stays clean. The manifesto rule: "stdout is for data, stderr is for messages."Testing under different capabilities
For local development, the easiest way to test capability paths:
# No color
NO_COLOR=1 my-cli deploy
# Force truecolor in CI
FORCE_COLOR=3 my-cli deploy
# Pipe — non-interactive
my-cli deploy | cat
# Dumb terminal
TERM=dumb my-cli deployEach variant should produce sensible output. If you find a component that emits escape codes through a pipe, it's a bug — file an issue.
Continue with Motion — the animation tokens that capability detection gates.