Skip to content

One terminal output-style policy: honor NO_COLOR / --color / tty for color and hyperlinks (#1135) - #1184

Merged
cscheid merged 6 commits into
mainfrom
bugfix/issue-1135
Nov 11, 2026
Merged

cscheid merged 6 commits into
mainfrom
bugfix/issue-1135

Conversation

@cscheid

@cscheid cscheid commented Nov 11, 2026

Copy link
Copy Markdown
Member

Fixes #1135 (braid: bd-be8ve52f).

Problem

q2 decided whether to emit ANSI color and OSC 8 hyperlinks in several places, and the places disagreed. Render diagnostics hard-coded both to on, so NO_COLOR=1 q2 render index.qmd 2>log wrote 162 SGR sequences and 2 OSC 8 links into the log. Three other paths had their own rules:

  • the -v tracing logs honored NO_COLOR but colored even when redirected to a file;
  • ParseError's Display baked styling into stored strings, so render aborts leaked escapes too (a broken _quarto.yml gave 62 SGR and 2 OSC 8);
  • pampa used the always-on library default.

The triage and the full audit are in claude-notes/issue-reports/1135/triage.md. The plan, with an end-to-end record, is in claude-notes/plans/2026-11-11-terminal-output-style.md.

Change

One policy, resolved once. The new quarto_util::output_style module holds a pure resolve() and a OnceLock that is set on the main thread right after argument parsing. It decides each stream separately; the first matching rule wins:

  1. --color=never|always (a new global flag on q2 and hub);
  2. NO_COLOR set to a non-empty value turns color off;
  3. CLICOLOR_FORCE or FORCE_COLOR (non-empty, not 0) turns color on;
  4. CLICOLOR=0 turns color off;
  5. otherwise color is on when the stream is a tty and TERM supports color, or CLICOLOR / CI is set.

This is anstream's precedence, so q2 agrees with clap's --help.

Hyperlinks have their own rule. They need color, a tty, and a terminal that supports-hyperlinks recognizes, or an explicit FORCE_HYPERLINK. Forcing color into a pipe never adds links. get() returns plain output when init never ran (libraries, tests, WASM).

Every sink reads the policy. That covers:

  • render diagnostics and the counts clause;
  • CLI diagnostics;
  • create / add / use;
  • the tracing layer (with_ansi);
  • hub, pampa, qmd-syntax-helper and quarto-preview.

No styling baked into strings. ParseError::render_with(&TextRenderOptions) is new. render() and Display are now plain. Terminal sinks render with the resolved style:

  • the abort paths;
  • RenderAbort::user_message;
  • main, which now wraps run() and prints a parse error through terminal_style::format_error_report. Every other error prints exactly as before.

String producers (WASM entry points, ts_record) now pass plain() explicitly.

Docs. New page: docs/tools/terminal-output.qmd.

Behavior changes for reviewers

  • Fewer links. Terminals that supports-hyperlinks doesn't recognize no longer get OSC 8 links unless FORCE_HYPERLINK=1 is set. Examples are tmux and macOS Terminal.app. Before this change, links appeared wherever color did.
  • Updated test. json_errors::ipynb_diagnostic_hyperlinks_real_notebook pinned "hyperlinks into a captured stderr". It now asks for them with FORCE_HYPERLINK=1.
  • main's error output. A parse error that reaches main no longer prints a doubled Error: header or repeats itself under Caused by:.
  • --help. q2 --help follows the environment variables but not --color, because clap renders help before our flag is parsed. The docs say so.

The upstream default (TextRenderOptions::default() is styled) is out of scope here. It is filed as posit-dev/quarto-error-reporting#12.

Tests (written first, seen failing)

  • 20 resolver unit tests covering the precedence table.
  • 13 binary-driven q2 tests (tests/integration/terminal_output_style.rs): diagnostics, tracing, --color, the force and hyperlink variables, and the Dispatch and Scripts abort paths. Each abort path has a pipe test, which failed before the change, and a --color=always guard, which passed before and still passes, so no terminal path lost color.
  • 2 binary-driven pampa tests.
  • Unit tests for ParseError::{Display, render_with} and format_error_report.

Verification

  • Ran the q2 render ignores NO_COLOR and prints colour codes and hyperlinks in its diagnostics #1135 repro through the binary, writing to a file: 0 SGR / 0 OSC 8, with or without NO_COLOR.
  • Simulated a terminal with script. The abort and diagnostic outputs keep their before-change SGR / OSC 8 counts (I compared counts, not bytes).
  • cargo xtask verify --skip-rust-tests passes, including the hub-client WASM build and the JS/TS suites.
  • cargo nextest run --workspace --no-fail-fast: 19355 of 19356 pass. The one failure is local-only (R/knitr rlang_error, bd-54odm3rz) and also fails on clean main.
  • Clippy -D warnings passes on all touched crates. The Windows cross-clippy of quarto-util is clean. I did not cross-check the other crates for Windows; their changes contain no platform-specific code.

🤖 Generated with Claude Code

cscheid and others added 6 commits November 11, 2026 14:10
…ve52f)

Duplicate of bd-be8ve52f. Reproduced: 162 SGR + 2 OSC 8 sequences on a
redirected stderr, unchanged by NO_COLOR=1. Audit found four disagreeing
terminal-styling policies (render diagnostics hard-coded on, summary
clause correct, tracing ignores tty, ParseError Display bakes ANSI into
stored strings) and recommends one resolved-once OutputStyle following
NO_COLOR / CLICOLOR_FORCE / FORCE_COLOR / --color conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maintainer-approved shape from the #1135 triage: resolve color/hyperlink
style once, early on the main thread, from --color / NO_COLOR /
CLICOLOR(_FORCE) / FORCE_COLOR / tty; thread it to every sink; stop
baking ANSI into stored strings behind regression tests; document it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f, GH #1135)

q2 decided ANSI color and OSC 8 hyperlinks in several disagreeing
places; render diagnostics hard-coded both on, so `NO_COLOR=1` and a
redirected stderr still got 162 SGR + 2 OSC 8 sequences (GH #1135).

- quarto-util: new `output_style` module. A pure `resolve` decides per
  stream from `--color`, NO_COLOR, CLICOLOR_FORCE/FORCE_COLOR,
  CLICOLOR=0, tty and TERM (anstream's precedence); hyperlinks need
  color + tty + a terminal known to support OSC 8, or FORCE_HYPERLINK.
  `init` runs once on the main thread; `get` is plain if never init'd.
- q2: global `--color <auto|always|never>`; init right after parsing;
  tracing layer uses `with_ansi`; render diagnostics, the counts
  clause, CLI diagnostics and create/add/use warnings all render with
  the resolved stderr style (new commands::terminal_style helper).
- hub: same `--color` flag + early init + `with_ansi`.
- pampa, qmd-syntax-helper, quarto-preview: follow the same policy.
- docs: new docs/tools/terminal-output.qmd page.

Tests: 20 resolver unit tests; 9 q2 + 2 pampa binary-driven tests
(all failed before the change). json_errors'
ipynb_diagnostic_hyperlinks_real_notebook pinned "hyperlinks into a
pipe" and now requests them with FORCE_HYPERLINK=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ParseError's Display rendered with the default TextRenderOptions (color
+ hyperlinks on), so every render abort printed through it leaked
escapes into pipes and NO_COLOR runs (a broken _quarto.yml: 62 SGR +
2 OSC 8), and every stored/forwarded string carried them.

- quarto-core: ParseError::render_with(&TextRenderOptions); render()
  and Display are now plain.
- q2: the abort sinks (DiscoverParse, RenderAbort::Parse, Scripts,
  user_message for `preview --static`) render with the resolved stderr
  style; main wraps run() and prints a parse error reaching it via
  terminal_style::format_error_report (bare, or anyhow's Caused-by
  layout), every other error exactly as before.
- quarto-preview: the pre-render script failure renders styled.
- String producers (ts_record, pampa DiagnosticCollector and WASM
  entry points, wasm-quarto-hub-client) pass TextRenderOptions::plain().

Guards written first: pipe tests (failed before) and --color=always
tests (passed before, still pass) for the Dispatch and Scripts aborts;
unit tests for render_with/Display and format_error_report. In a
terminal the abort output has the same SGR/OSC 8 counts as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cscheid
cscheid merged commit 1f71375 into main Nov 11, 2026
17 checks passed
@cscheid
cscheid deleted the bugfix/issue-1135 branch November 11, 2026 23:14
cscheid added a commit that referenced this pull request Nov 12, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

q2 render ignores NO_COLOR and prints colour codes and hyperlinks in its diagnostics

2 participants