refactor(cli): Replace tracing-indicatif with shared MultiProgress
tracing-indicatif pulled in vt100, arrayvec, and its own indicatif integration layer. Replace it with a minimal SuspendingWriter that coordinates tracing output with progress bars via a global LazyLock MultiProgress. - Add src/cli/progress.rs: shared MultiProgress singleton via LazyLock and a SuspendingWriter that suspends bars before writing log lines, preventing interleaving/flicker - Wire all progress bar creation through multi().add() in sync and ingest commands - Replace IndicatifLayer in main.rs with SuspendingWriter for tracing-subscriber's fmt layer - Remove tracing-indicatif from Cargo.toml (drops vt100 and arrayvec transitive deps) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
41
Cargo.lock
generated
41
Cargo.lock
generated
@@ -76,12 +76,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
@@ -960,7 +954,6 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unit-prefix",
|
||||
"vt100",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
@@ -1115,7 +1108,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
@@ -2032,18 +2024,6 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1ef6990e0438749f0080573248e96631171a0b5ddfddde119aa5ba8c3a9c47e"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@@ -2174,27 +2154,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vt100"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
||||
@@ -53,7 +53,6 @@ libc = "0.2"
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-indicatif = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -40,7 +40,7 @@ fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressB
|
||||
if robot_mode {
|
||||
return ProgressBar::hidden();
|
||||
}
|
||||
let pb = ProgressBar::new_spinner();
|
||||
let pb = crate::cli::progress::multi().add(ProgressBar::new_spinner());
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.blue} {msg}")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! CLI module with clap command definitions.
|
||||
|
||||
pub mod commands;
|
||||
pub mod progress;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
119
src/cli/progress.rs
Normal file
119
src/cli/progress.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! Shared progress bar infrastructure.
|
||||
//!
|
||||
//! All progress bars must be created via [`multi()`] to ensure coordinated
|
||||
//! rendering. The [`SuspendingWriter`] suspends the multi-progress before
|
||||
//! writing tracing output, preventing log lines from interleaving with
|
||||
//! progress bar animations.
|
||||
|
||||
use indicatif::MultiProgress;
|
||||
use std::io::Write;
|
||||
use std::sync::LazyLock;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
/// Global multi-progress that coordinates all progress bar rendering.
|
||||
///
|
||||
/// Every `ProgressBar` displayed to the user **must** be registered via
|
||||
/// `multi().add(bar)`. Standalone bars bypass the coordination and will
|
||||
/// fight with other bars for the terminal line, causing rapid flashing.
|
||||
static MULTI: LazyLock<MultiProgress> = LazyLock::new(MultiProgress::new);
|
||||
|
||||
/// Returns the shared [`MultiProgress`] instance.
|
||||
pub fn multi() -> &'static MultiProgress {
|
||||
&MULTI
|
||||
}
|
||||
|
||||
/// A tracing `MakeWriter` that suspends the shared [`MultiProgress`] while
|
||||
/// writing, so log output doesn't interleave with progress bar animations.
|
||||
///
|
||||
/// # How it works
|
||||
///
|
||||
/// `MultiProgress::suspend` temporarily clears all active progress bars from
|
||||
/// the terminal, executes the closure (which writes the log line), then
|
||||
/// redraws the bars. This ensures a clean, flicker-free display even when
|
||||
/// logging happens concurrently with progress updates.
|
||||
#[derive(Clone)]
|
||||
pub struct SuspendingWriter;
|
||||
|
||||
/// Writer returned by [`SuspendingWriter`] that buffers a single log line
|
||||
/// and flushes it inside a `MultiProgress::suspend` call.
|
||||
pub struct SuspendingWriterInner {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Write for SuspendingWriterInner {
|
||||
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
|
||||
self.buf.extend_from_slice(data);
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
// Nothing to do — actual flush happens on drop.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SuspendingWriterInner {
|
||||
fn drop(&mut self) {
|
||||
if !self.buf.is_empty() {
|
||||
let buf = std::mem::take(&mut self.buf);
|
||||
MULTI.suspend(|| {
|
||||
let _ = std::io::stderr().write_all(&buf);
|
||||
let _ = std::io::stderr().flush();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MakeWriter<'a> for SuspendingWriter {
|
||||
type Writer = SuspendingWriterInner;
|
||||
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
SuspendingWriterInner { buf: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indicatif::ProgressBar;
|
||||
|
||||
#[test]
|
||||
fn multi_returns_same_instance() {
|
||||
let a = multi() as *const MultiProgress;
|
||||
let b = multi() as *const MultiProgress;
|
||||
assert!(std::ptr::eq(a, b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_bar_is_tracked() {
|
||||
let bar = multi().add(ProgressBar::new_spinner());
|
||||
bar.set_message("test");
|
||||
bar.finish_and_clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_bar_works_with_multi() {
|
||||
let bar = multi().add(ProgressBar::hidden());
|
||||
bar.set_message("hidden");
|
||||
bar.finish_and_clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspending_writer_buffers_and_flushes() {
|
||||
let writer = SuspendingWriter;
|
||||
let mut w = MakeWriter::make_writer(&writer);
|
||||
// Write should succeed and buffer data
|
||||
let n = w.write(b"test log line\n").unwrap();
|
||||
assert_eq!(n, 14);
|
||||
// Drop flushes via suspend — no panic means it works
|
||||
drop(w);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspending_writer_empty_does_not_flush() {
|
||||
let writer = SuspendingWriter;
|
||||
let w = MakeWriter::make_writer(&writer);
|
||||
// Drop with empty buffer — should be a no-op
|
||||
drop(w);
|
||||
}
|
||||
}
|
||||
@@ -41,21 +41,20 @@ async fn main() {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
// Initialize logging with indicatif support for clean progress bar output
|
||||
let indicatif_layer = tracing_indicatif::IndicatifLayer::new();
|
||||
|
||||
// Initialize logging with progress-bar-aware writer.
|
||||
// SuspendingWriter suspends the shared MultiProgress before each log line,
|
||||
// preventing log output from interleaving with progress bar animations.
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_writer(indicatif_layer.get_stderr_writer()),
|
||||
.with_writer(lore::cli::progress::SuspendingWriter),
|
||||
)
|
||||
.with(
|
||||
EnvFilter::from_default_env()
|
||||
.add_directive("lore=info".parse().unwrap())
|
||||
.add_directive("warn".parse().unwrap()),
|
||||
)
|
||||
.with(indicatif_layer)
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
Reference in New Issue
Block a user