Skip to content

feat(cli): color help and error labels with one --color policy - #86

Merged
thebrandonlucas merged 1 commit into
masterfrom
feat/cli-look-01-color
Oct 23, 2026
Merged

thebrandonlucas merged 1 commit into
masterfrom
feat/cli-look-01-color

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Stack position: 1 of 13 in the look-and-feel stack. Base: master. Every later layer builds on the style module this PR adds.

Problem

The CLI's own output had no color. clap already colored help and parse errors on a terminal, so a mistyped flag printed a red error: but an API error printed a plain one. No flag controlled either.

Behavior

Input Help and parse errors (clap) error: hint: details: (ours) JSON, NDJSON, completions
terminal, no settings colored colored never colored
redirected plain plain never colored
--color always colored colored never colored
--color never or --no-color plain plain never colored
NO_COLOR=1 plain (flag still wins) plain (flag still wins) never colored
CLICOLOR_FORCE=1 colored colored never colored
  • New global --color auto|always|never under Output, plus a hidden --no-color. Auto uses anstream's exact rule, the one clap already used: NO_COLOR, CLICOLOR_FORCE, CLICOLOR, TERM=dumb, and whether each stream is a terminal.
  • Help styles follow cargo and use only the 16 basic colors, so the terminal theme keeps them legible: green bold headers, cyan bold literals, cyan placeholders.
  • Colors: error: red bold, hint: cyan bold, details: dim. Only the label is colored, and the words are unchanged.
  • style::paint accepts only &'static str labels, so API text cannot be styled. Every escape sequence comes from the palette, and a unit test checks this with hostile text (CSI, OSC 8, BEL).

Design notes

  • No mutable global. docs/quality.md forbids global state, so the code does not call anstream::ColorChoice::write_global. Startup pre-scans argv for --color and --no-color, stopping at -- (like the existing --json scan). It passes the result to clap through Command::color and to our stderr through anstream::AutoStream::new. After parsing, the typed flag decides runtime errors.
  • Dependency. This adds anstream = "=1.0.0" directly. clap already depends on it, so Cargo.lock gains one dependency edge and no crates. Styles use clap's builder::styling re-export of anstyle.
  • Human errors are now written in one checked write. Before, eprintln! would panic if stderr was closed.
  • FORCE_COLOR is not honored. Its meaning is still an open question in the plan.

Validation

  • cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata (0.9.2), cargo test --locked (135 unit and 122 integration tests), and python3 scripts/check-coverage.py all pass.
  • New integration tests cover the flag and environment matrix, check that stripped colored output equals the plain output, check that JSON errors are byte-identical under --color always with CLICOLOR_FORCE=1, and check colored and plain help and parse errors.
  • The integration harness now removes NO_COLOR, CLICOLOR, and CLICOLOR_FORCE, so a developer's environment cannot change the bytes the tests compare.
  • CRAP: 0 of 619 measured functions at or above 20. The highest is 19.60, src/api.rs wait_for_payment, which this PR does not touch.
  • Manual check: rendered --help and a runtime error on a pseudo-terminal (script); both colored as described.

Docs

docs/reference/output.md, environment.md (NO_COLOR, CLICOLOR_FORCE, CLICOLOR), the regenerated global-flags.md, and CHANGELOG.md.

tee8z
tee8z previously approved these changes Oct 22, 2026

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved 1db2da6 on its base, 520c775. The PR conflicts with current master in CHANGELOG.md and src/cli.rs, because #87–#95 merged after it was cut, so it needs a rebase before it can merge. A rebase dismisses this approval, and I will compare the trees again then.

The color policy is sound:

  • The argv pre-scan stops at --, the last --color wins, and an invalid value falls back to auto.
  • --no-color and --color conflict.
  • Help styles reach each subcommand through app_ext.
  • The human error report is scrubbed before it is painted, so every escape comes from the palette, and the hostile-text unit test pins that.
  • Redirected stderr goes through anstream's strip path, so its bytes are unchanged, and JSON output never passes through style.
  • The environment docs match anstyle-query's handling of NO_COLOR and CLICOLOR_FORCE.

Nonblocking:

  • startup.rs:68 builds the --json parse-error envelope from error.to_string() on a clap error that may be colored. It is plain today, because StyledStr's Display drops the styles. But the byte-identical JSON test covers only a runtime error. A case such as voltage --json --color always paymnts with CLICOLOR_FORCE=1, asserting no ESC byte, would pin the parse path.
  • docs/quality.md assigns each concern to a module, and it has no entry for the new style module.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (135 unit, 122 integration) and python3 scripts/check-coverage.py passed. CRAP: 619 measured functions, none at or above 20, highest wait_for_payment at 19.60; 177 functions were unmeasured. CI is green on this head.

@thebrandonlucas

Copy link
Copy Markdown
Contributor Author

Rebased the whole stack onto master (#87–#95) and fixed the review findings on #91, #96, #98, #101, and #108. The rebase changed this PR's head, which reset its approval. The only conflicts were import lines in src/cli.rs and the CHANGELOG, which now uses master's flat Unreleased list. Every layer passed fmt, clippy, machete, tests, and the coverage check. CRAP has no violations on the changed layers or the top (819 functions measured, highest 18.00). All 13 commits are signed.

`--color auto|always|never` (and a hidden `--no-color`) decides; otherwise each
stream follows anstream's automatic choice, which honors NO_COLOR,
CLICOLOR_FORCE, CLICOLOR, TERM=dumb, and whether the stream is a terminal.
clap's help and parse errors use the same choice and cargo-style 16-color
styles. Only fixed labels are painted, so API text never carries an escape
sequence, and JSON output is never colored.

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved 6c8a7f6, a rebase onto master 8dfa4ee. The patch matches 1db2da6, which I approved, and only the CHANGELOG context changed. It also merges cleanly with the current master (e134cf1, with #102). Validation on this head, in nix develop: the full gate passed (147 unit, 125 integration). CRAP: 646 measured functions, none at or above 20, highest wait_for_payment at 19.60. CI is green.

@tee8z

tee8z commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

A note on my approval: it was recorded on 97e4ea1, not the 6c8a7f6 that the review body names. 97e4ea1 is the same patch rebased onto master e134cf1, and only the CHANGELOG context differs, so the approval covers it. I am running the gate on 97e4ea1 now.

@thebrandonlucas
thebrandonlucas merged commit 8d6c1e3 into master Oct 23, 2026
7 of 8 checks passed
@thebrandonlucas
thebrandonlucas deleted the feat/cli-look-01-color branch October 23, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants