When quarto's stderr is piped, redirected to a file, or consumed by an editor build tool (e.g. Emacs M-x compile, VS Code tasks), progress and spinner helpers still emit ANSI cursor-control sequences. These appear as raw escape bytes in the output buffer:
^[[0G^[[2K^[[J^[[0G^[[2K^[[J^[[0G
Reported in https://github.com/orgs/quarto-dev/discussions/14403
Root cause
src/core/console.ts emits cursor-control bytes without checking whether the output stream is a terminal. Specifically, clearLine() calls ansi.eraseLine.cursorLeft() unconditionally, and spinner() and progressBar() gate their output on runningInCI() but not on whether stderr is actually a TTY. withSpinner()'s cancel path always calls clearLine(), regardless of where output goes.
|
export function clearLine() { |
|
info(ansi.eraseLine.cursorLeft(), { newline: false }); |
|
} |
|
const id = setInterval(() => { |
|
// Display the message |
|
const char = kSpinnerChars[spin % kSpinnerChars.length]; |
|
const msg = `${spinContainer(char)} ${statusFn()}`; |
|
|
|
// when running in CI only show the first tick |
|
if (!runningInCI() || spin === 0) { |
|
info(`\r${msg}`, { |
|
newline: false, |
|
}); |
|
} |
|
|
|
// Increment the spin counter |
|
spin = spin + 1; |
|
}, timeInterval); |
NO_COLOR does not cover this — it governs colors only. --log-format plain governs the log file format, not console output.
The codebase already has the right helper: isInteractiveTerminal() in src/core/platform.ts, which returns Deno.stderr.isTerminal(). It is already used in command/create/cmd.ts and command/publish/cmd.ts to gate interactive prompts, but not in src/core/console.ts.
|
export function isInteractiveTerminal() { |
|
return Deno.stderr.isTerminal(); |
|
} |
src/core/log.ts's LogFileHandler.format() already handles this for file output — messages starting with \r are dropped because they are progress lines. The same "progress is terminal-only" notion is not applied to the stderr console handler or to clearLine().
|
override format(logRecord: LogRecord): string { |
|
// Messages that start with a carriage return are progress messages |
|
// that rewrite a line, so just ignore these |
|
if (logRecord.msg.startsWith("\r")) { |
|
return ""; |
|
} |
Reproduction
Any quarto command that uses withSpinner or progressBar shows this when stderr is piped. The cleanest CLI-only form:
quarto check versions 2>&1 | cat -v
quarto install tinytex 2>&1 | cat -v
leading to something like
$ quarto check versions 2>&1 | cat -v
Quarto 1.10.3
^M[>] Checking versions of quarto binary dependencies...
Pandoc version 3.8.3: OK
Dart Sass version 1.87.0: OK
Deno version 2.4.5: OK
Typst version 0.14.2: OK
^M[>] Checking versions of quarto dependencies......OK
Or via Emacs M-x compile running quarto render example.qmd --to html, where the compilation buffer is not a TTY.
Suggested direction
Gate clearLine(), spinner(), and progressBar() on isInteractiveTerminal() (extending the !runningInCI() check that is already there to also require a TTY). This matches the idiom already used for interactive prompts in create/publish, and aligns console behavior with the file-handler behavior in log.ts.
When
quarto's stderr is piped, redirected to a file, or consumed by an editor build tool (e.g. EmacsM-x compile, VS Code tasks), progress and spinner helpers still emit ANSI cursor-control sequences. These appear as raw escape bytes in the output buffer:Reported in https://github.com/orgs/quarto-dev/discussions/14403
Root cause
src/core/console.tsemits cursor-control bytes without checking whether the output stream is a terminal. Specifically,clearLine()callsansi.eraseLine.cursorLeft()unconditionally, andspinner()andprogressBar()gate their output onrunningInCI()but not on whether stderr is actually a TTY.withSpinner()'s cancel path always callsclearLine(), regardless of where output goes.quarto-cli/src/core/console.ts
Lines 180 to 182 in d5c1f1d
quarto-cli/src/core/console.ts
Lines 110 to 124 in d5c1f1d
NO_COLORdoes not cover this — it governs colors only.--log-format plaingoverns the log file format, not console output.The codebase already has the right helper:
isInteractiveTerminal()insrc/core/platform.ts, which returnsDeno.stderr.isTerminal(). It is already used incommand/create/cmd.tsandcommand/publish/cmd.tsto gate interactive prompts, but not insrc/core/console.ts.quarto-cli/src/core/platform.ts
Lines 97 to 99 in d5c1f1d
src/core/log.ts'sLogFileHandler.format()already handles this for file output — messages starting with\rare dropped because they are progress lines. The same "progress is terminal-only" notion is not applied to the stderr console handler or toclearLine().quarto-cli/src/core/log.ts
Lines 232 to 237 in d5c1f1d
Reproduction
Any quarto command that uses
withSpinnerorprogressBarshows this when stderr is piped. The cleanest CLI-only form:leading to something like
Or via Emacs
M-x compilerunningquarto render example.qmd --to html, where the compilation buffer is not a TTY.Suggested direction
Gate
clearLine(),spinner(), andprogressBar()onisInteractiveTerminal()(extending the!runningInCI()check that is already there to also require a TTY). This matches the idiom already used for interactive prompts increate/publish, and aligns console behavior with the file-handler behavior inlog.ts.