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",
|
"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]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -960,7 +954,6 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"unit-prefix",
|
"unit-prefix",
|
||||||
"vt100",
|
|
||||||
"web-time",
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1115,7 +1108,6 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-indicatif",
|
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
@@ -2032,18 +2024,6 @@ dependencies = [
|
|||||||
"valuable",
|
"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]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2174,27 +2154,6 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
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]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ libc = "0.2"
|
|||||||
# Logging
|
# Logging
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing-indicatif = "0.3"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressB
|
|||||||
if robot_mode {
|
if robot_mode {
|
||||||
return ProgressBar::hidden();
|
return ProgressBar::hidden();
|
||||||
}
|
}
|
||||||
let pb = ProgressBar::new_spinner();
|
let pb = crate::cli::progress::multi().add(ProgressBar::new_spinner());
|
||||||
pb.set_style(
|
pb.set_style(
|
||||||
ProgressStyle::default_spinner()
|
ProgressStyle::default_spinner()
|
||||||
.template("{spinner:.blue} {msg}")
|
.template("{spinner:.blue} {msg}")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! CLI module with clap command definitions.
|
//! CLI module with clap command definitions.
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod progress;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::io::IsTerminal;
|
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);
|
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize logging with indicatif support for clean progress bar output
|
// Initialize logging with progress-bar-aware writer.
|
||||||
let indicatif_layer = tracing_indicatif::IndicatifLayer::new();
|
// SuspendingWriter suspends the shared MultiProgress before each log line,
|
||||||
|
// preventing log output from interleaving with progress bar animations.
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(
|
.with(
|
||||||
tracing_subscriber::fmt::layer()
|
tracing_subscriber::fmt::layer()
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_writer(indicatif_layer.get_stderr_writer()),
|
.with_writer(lore::cli::progress::SuspendingWriter),
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
EnvFilter::from_default_env()
|
EnvFilter::from_default_env()
|
||||||
.add_directive("lore=info".parse().unwrap())
|
.add_directive("lore=info".parse().unwrap())
|
||||||
.add_directive("warn".parse().unwrap()),
|
.add_directive("warn".parse().unwrap()),
|
||||||
)
|
)
|
||||||
.with(indicatif_layer)
|
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|||||||
Reference in New Issue
Block a user