Repository navigation
One terminal output-style policy: honor NO_COLOR / --color / tty for color and hyperlinks (#1135) - #1184
Merged
Conversation
…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>
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
cscheid
added a commit
that referenced
this pull request
Nov 12, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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>logwrote 162 SGR sequences and 2 OSC 8 links into the log. Three other paths had their own rules:-vtracing logs honoredNO_COLORbut colored even when redirected to a file;ParseError'sDisplaybaked styling into stored strings, so render aborts leaked escapes too (a broken_quarto.ymlgave 62 SGR and 2 OSC 8);pampaused 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 inclaude-notes/plans/2026-11-11-terminal-output-style.md.Change
One policy, resolved once. The new
quarto_util::output_stylemodule holds a pureresolve()and aOnceLockthat is set on the main thread right after argument parsing. It decides each stream separately; the first matching rule wins:--color=never|always(a new global flag onq2andhub);NO_COLORset to a non-empty value turns color off;CLICOLOR_FORCEorFORCE_COLOR(non-empty, not0) turns color on;CLICOLOR=0turns color off;TERMsupports color, orCLICOLOR/CIis 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-hyperlinksrecognizes, or an explicitFORCE_HYPERLINK. Forcing color into a pipe never adds links.get()returns plain output wheninitnever ran (libraries, tests, WASM).Every sink reads the policy. That covers:
create/add/use;with_ansi);hub,pampa,qmd-syntax-helperandquarto-preview.No styling baked into strings.
ParseError::render_with(&TextRenderOptions)is new.render()andDisplayare now plain. Terminal sinks render with the resolved style:RenderAbort::user_message;main, which now wrapsrun()and prints a parse error throughterminal_style::format_error_report. Every other error prints exactly as before.String producers (WASM entry points,
ts_record) now passplain()explicitly.Docs. New page:
docs/tools/terminal-output.qmd.Behavior changes for reviewers
supports-hyperlinksdoesn't recognize no longer get OSC 8 links unlessFORCE_HYPERLINK=1is set. Examples are tmux and macOS Terminal.app. Before this change, links appeared wherever color did.json_errors::ipynb_diagnostic_hyperlinks_real_notebookpinned "hyperlinks into a captured stderr". It now asks for them withFORCE_HYPERLINK=1.main's error output. A parse error that reachesmainno longer prints a doubledError:header or repeats itself underCaused by:.--help.q2 --helpfollows 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)
q2tests (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=alwaysguard, which passed before and still passes, so no terminal path lost color.pampatests.ParseError::{Display, render_with}andformat_error_report.Verification
q2 renderignoresNO_COLORand prints colour codes and hyperlinks in its diagnostics #1135 repro through the binary, writing to a file: 0 SGR / 0 OSC 8, with or withoutNO_COLOR.script. The abort and diagnostic outputs keep their before-change SGR / OSC 8 counts (I compared counts, not bytes).cargo xtask verify --skip-rust-testspasses, 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/knitrrlang_error, bd-54odm3rz) and also fails on cleanmain.-D warningspasses on all touched crates. The Windows cross-clippy ofquarto-utilis clean. I did not cross-check the other crates for Windows; their changes contain no platform-specific code.🤖 Generated with Claude Code