Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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; anstream strips 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, or TERM=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 raw println!) so the gate is honoured, a discipline to enforce in review. anstream replaces direct println! 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_COLOR detection and not already in the tree; we would re-implement what anstream gives for free.
  • termcolor: solid (used by ripgrep/cargo) but a heavier API and not the clap stack; would be a new dependency.
  • colored / console: colored mutates global state and lags on NO_COLOR; console (from indicatif) 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.