From bbf28f486aa42278a473f6c994e6fb2cd9262dc2 Mon Sep 17 00:00:00 2001 From: Nick DiZazzo Date: Sun, 13 Sep 2026 19:09:00 -0400 Subject: [PATCH] refactor(progress): write inline progress directly to the stderr handle The console-print ratchet forbids the print macros, not console output. The two inline progress renderers were the last approved occurrences, so they now take a single `std::io::stderr()` handle, `write!` into it, and flush once, which empties the ratchet without a permanent exemption. Both copies of the renderer are fixed in place rather than collapsed into one, because the two `fit_inline_gauge_label` implementations genuinely differ on very narrow terminals and de-duplicating them would change what the CLI prints. The dedup is worth doing on its own terms, not under cover of a plumbing change. Each renderer now documents why holding a terminal handle directly is legitimate here, so a later pass that forbids direct handles in library crates has a named exception to point at. --- .../mesh-llm-events/src/terminal_progress.rs | 43 ++++++++++++++----- crates/mesh-llm-tui/src/terminal_progress.rs | 43 ++++++++++++++----- tools/xtask/data/console_print_allowlist.json | 31 +------------ 3 files changed, 67 insertions(+), 50 deletions(-) diff --git a/crates/mesh-llm-events/src/terminal_progress.rs b/crates/mesh-llm-events/src/terminal_progress.rs index 6e713f851b..110927c5d1 100644 --- a/crates/mesh-llm-events/src/terminal_progress.rs +++ b/crates/mesh-llm-events/src/terminal_progress.rs @@ -1,3 +1,22 @@ +//! Inline terminal progress rendering for one-shot commands. +//! +//! This module is an implementation of the console output facility itself, so +//! it is one of the few places allowed to hold a `std::io::stderr()` handle +//! directly. Library code elsewhere must route operational output through +//! `OutputEvent`, `tracing`, or the sink-aware console writer; writing straight +//! to the terminal there would bypass the TUI and corrupt the dashboard, and +//! would put free-form text on the stream while a JSON sink is installed. +//! +//! The direct writes here are legitimate because: +//! +//! - every entry point early-returns on [`crate::json_mode_enabled`], so no +//! progress bytes are emitted while a JSON sink is installed; +//! - progress is a transient, cursor-addressed redraw (`\r` + `\x1b[2K`) rather +//! than a log record, so it has no meaningful structured representation and +//! must not be routed back through the sink that would render it; +//! - routing this module's output through the writer would be circular — the +//! writer is the consumer of this renderer, not the other way round. + use anyhow::{Context, Result}; use crossterm::terminal::size as terminal_size; use ratatui::{ @@ -22,10 +41,9 @@ pub fn clear_stderr_line() -> Result<()> { if crate::json_mode_enabled() { return Ok(()); } - eprint!("\r\x1b[2K"); - std::io::stderr() - .flush() - .context("Flush terminal progress clear")?; + let mut stderr = std::io::stderr(); + write!(stderr, "\r\x1b[2K").context("Write terminal progress clear")?; + stderr.flush().context("Flush terminal progress clear")?; Ok(()) } @@ -69,8 +87,14 @@ pub fn start_spinner(message: &str) -> SpinnerHandle { .lock() .map(|guard| guard.clone()) .unwrap_or_else(|_| "Working".to_string()); - eprint!("\r\x1b[2K{} {}", frames[index % frames.len()], current); - let _ = std::io::stderr().flush(); + let mut stderr = std::io::stderr(); + let _ = write!( + stderr, + "\r\x1b[2K{} {}", + frames[index % frames.len()], + current + ); + let _ = stderr.flush(); index += 1; thread::sleep(Duration::from_millis(120)); } @@ -115,10 +139,9 @@ impl DeterminateProgressLine { self.prefix, label, percent, current, total, detail ), ); - eprint!("\r\x1b[2K{gauge}"); - std::io::stderr() - .flush() - .context("Flush determinate progress")?; + let mut stderr = std::io::stderr(); + write!(stderr, "\r\x1b[2K{gauge}").context("Write determinate progress")?; + stderr.flush().context("Flush determinate progress")?; Ok(()) } } diff --git a/crates/mesh-llm-tui/src/terminal_progress.rs b/crates/mesh-llm-tui/src/terminal_progress.rs index d7e41f86f4..4797af52ec 100644 --- a/crates/mesh-llm-tui/src/terminal_progress.rs +++ b/crates/mesh-llm-tui/src/terminal_progress.rs @@ -1,3 +1,22 @@ +//! Inline terminal progress rendering for one-shot commands. +//! +//! This module is an implementation of the console output facility itself, so +//! it is one of the few places allowed to hold a `std::io::stderr()` handle +//! directly. Library code elsewhere must route operational output through +//! `OutputEvent`, `tracing`, or the sink-aware console writer; writing straight +//! to the terminal there would bypass the TUI and corrupt the dashboard, and +//! would put free-form text on the stream while a JSON sink is installed. +//! +//! The direct writes here are legitimate because: +//! +//! - every entry point early-returns on [`crate::json_mode_enabled`], so no +//! progress bytes are emitted while a JSON sink is installed; +//! - progress is a transient, cursor-addressed redraw (`\r` + `\x1b[2K`) rather +//! than a log record, so it has no meaningful structured representation and +//! must not be routed back through the sink that would render it; +//! - routing this module's output through the writer would be circular — the +//! writer is the consumer of this renderer, not the other way round. + use anyhow::{Context, Result}; use crossterm::terminal::size as terminal_size; use ratatui::{ @@ -22,10 +41,9 @@ pub fn clear_stderr_line() -> Result<()> { if crate::json_mode_enabled() { return Ok(()); } - eprint!("\r\x1b[2K"); - std::io::stderr() - .flush() - .context("Flush terminal progress clear")?; + let mut stderr = std::io::stderr(); + write!(stderr, "\r\x1b[2K").context("Write terminal progress clear")?; + stderr.flush().context("Flush terminal progress clear")?; Ok(()) } @@ -69,8 +87,14 @@ pub fn start_spinner(message: &str) -> SpinnerHandle { .lock() .map(|guard| guard.clone()) .unwrap_or_else(|_| "Working".to_string()); - eprint!("\r\x1b[2K{} {}", frames[index % frames.len()], current); - let _ = std::io::stderr().flush(); + let mut stderr = std::io::stderr(); + let _ = write!( + stderr, + "\r\x1b[2K{} {}", + frames[index % frames.len()], + current + ); + let _ = stderr.flush(); index += 1; thread::sleep(Duration::from_millis(120)); } @@ -115,10 +139,9 @@ impl DeterminateProgressLine { self.prefix, label, percent, current, total, detail ), ); - eprint!("\r\x1b[2K{gauge}"); - std::io::stderr() - .flush() - .context("Flush determinate progress")?; + let mut stderr = std::io::stderr(); + write!(stderr, "\r\x1b[2K{gauge}").context("Write determinate progress")?; + stderr.flush().context("Flush determinate progress")?; Ok(()) } } diff --git a/tools/xtask/data/console_print_allowlist.json b/tools/xtask/data/console_print_allowlist.json index 2e24e06792..9e26dfeeb6 100644 --- a/tools/xtask/data/console_print_allowlist.json +++ b/tools/xtask/data/console_print_allowlist.json @@ -1,30 +1 @@ -{ - "crates/mesh-llm-events/src/terminal_progress.rs": [ - { - "line": 25, - "macro_name": "eprint!" - }, - { - "line": 72, - "macro_name": "eprint!" - }, - { - "line": 118, - "macro_name": "eprint!" - } - ], - "crates/mesh-llm-tui/src/terminal_progress.rs": [ - { - "line": 25, - "macro_name": "eprint!" - }, - { - "line": 72, - "macro_name": "eprint!" - }, - { - "line": 118, - "macro_name": "eprint!" - } - ] -} \ No newline at end of file +{} \ No newline at end of file