ADR-019: Terminal styling via anstream + anstyle, rich-by-default with a --plain opt-out
Date: 2026-06-05 Status: Accepted Author: Rômulo Febasi
Context
Audited in the v0.5 code: the CLI output is essentially monochrome plain text.
The only styled output in the whole tool is the red error: label in
src/output.rs::print_error (gated on stderr.is_terminal()); everything else (success messages, search, list, info, doctor, graph) is bare
println!. There is no colour, no glyphs (✓/✗/▲), no tables, and none of the
clig.dev colour-disable signals (NO_COLOR, TERM=dumb,
--no-color) are honoured. The brief
docs/researchs/2026-06-05-cli-ux-and-agent-ergonomics.md set the target: a
premium (“AAA”) modern-CLI experience by default, while staying safe for
pipes and agents.
This requires a styling library and a clear policy. Two constraints shape the choice: (1) no new dependency without an ADR (CLAUDE.md), and (2) the output must never regress the data a piped consumer or an AI agent already parses.
A relevant finding: anstream, anstyle, anstyle-query and colorchoice
are already in Cargo.lock: clap pulls them through its default color
feature. Adopting them as direct dependencies adds zero new transitive
weight; it only promotes what is already compiled in.
Decision
Adopt anstyle (the styling vocabulary) + anstream (the
auto-stripping output stream) as direct dependencies for all terminal styling.
They are the clap/cargo ecosystem stack and are already in the tree.
Rendering is rich by default (opt-out model) behind a single predicate,
output::is_rich(), true only when all of:
- stdout is a TTY (checked per-stream;
anstreamstrips ANSI on non-TTY); - the output format is
text(never style--format json); - no opt-out is set:
--plain/--raw,SYNAPSE_PLAIN,--no-color,NO_COLOR(non-empty),SYNAPSE_NO_COLOR, orTERM=dumb.
--plain/--raw (and SYNAPSE_PLAIN) force raw/minimal output: no
colour, no tables/trees/panels, no animation: even on a TTY, for token economy
(an agent reading the human terminal) and minimalists. --no-color kills only
colour; --quiet only suppresses non-essential lines. A SYNAPSE_AGENT=1
convenience flips --plain + --no-input. Animations never run off a TTY
(clig.dev). This contract governs the rich renderers (tables, unicode trees,
panels, progress, animation) tracked under SYN-186/190/191/192/193/195.
Consequences
- Better: one centralised gate; a genuinely premium default UX; full
clig.dev compliance; agents/scripts keep stable plain output via auto-strip +
explicit
--plain. No new dependency weight. - Worse: every styled call site must route through
output::(not rawprintln!) so the gate is honoured, a discipline to enforce in review.anstreamreplaces directprintln!for styled output. - Open / revisit: table and tree rendering may want a dedicated crate
(
comfy-table/tabled): decided per SYN-190/193, not here. Progress animation crate (indicatif) is a separate, later decision (SYN-191/192) and will get its own ADR if it lands as a dependency.
Alternatives considered
owo-colors: ergonomic, zero-dep, but BYO TTY/NO_COLORdetection and not already in the tree; we would re-implement whatanstreamgives for free.termcolor: solid (used by ripgrep/cargo) but a heavier API and not the clap stack; would be a new dependency.colored/console:coloredmutates global state and lags onNO_COLOR;console(fromindicatif) is heavier and oriented at full TUI.crossterm: full terminal control (raw mode, events); overkill for styled line output and a large new dependency. Reserve for a future TUI (SYN-203, backlog), not the CLI line renderer.- Hand-rolled ANSI (the status quo, one
\x1b[31m): does not scale to a styling system and cannot honour the five clig.dev disable signals cleanly.