From b3ad0ab3e03106e34b3a757353c70ec63e3633e6 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sat, 3 Oct 2026 00:06:22 +0200 Subject: [PATCH] feat(cli): render library progress with indicatif The CLI owns the terminal now: TermProgress turns the library's Progress events into bars and spinners on stderr (hidden by indicatif when stderr is not a terminal). Named transfers (OCI layers, kernel, tarball) render as byte bars, extraction and initramfs as file-count spinners. --- Cargo.lock | 1 + crates/ecr-cli/Cargo.toml | 3 ++ crates/ecr-cli/src/main.rs | 11 ++-- crates/ecr-cli/src/progress.rs | 91 ++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 crates/ecr-cli/src/progress.rs diff --git a/Cargo.lock b/Cargo.lock index 2e0ce8e..dcdcca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,7 @@ dependencies = [ "anyhow", "clap", "ecr", + "indicatif", ] [[package]] diff --git a/crates/ecr-cli/Cargo.toml b/crates/ecr-cli/Cargo.toml index 4295632..60a4693 100644 --- a/crates/ecr-cli/Cargo.toml +++ b/crates/ecr-cli/Cargo.toml @@ -21,3 +21,6 @@ clap = { version = "4", features = ["derive", "env"] } # Error handling anyhow = "1" + +# Progress bar rendering of library progress events +indicatif = "0.18" diff --git a/crates/ecr-cli/src/main.rs b/crates/ecr-cli/src/main.rs index df1aea6..0e638cc 100644 --- a/crates/ecr-cli/src/main.rs +++ b/crates/ecr-cli/src/main.rs @@ -1,4 +1,5 @@ mod cli; +mod progress; use std::path::{Path, PathBuf}; @@ -11,6 +12,7 @@ use ecr::config::Config; use ecr::exec::ExecOptions; use ecr::mount::BindTarget; use ecr::{kernel, qemu, qemu_vm, utils, veprintln, verbose, PrepareRequest, RootfsCache}; +use progress::TermProgress; fn main() -> Result<()> { let args = Args::parse(); @@ -18,6 +20,9 @@ fn main() -> Result<()> { // Initialise verbosity before anything else so all downstream code can use veprintln!. verbose::set(args.verbose); + // Render library progress (bars/spinners) on the terminal + let progress = TermProgress::new(); + // Load config file let config = Config::load()?; @@ -38,7 +43,7 @@ fn main() -> Result<()> { arch: args.arch.clone().unwrap_or_default(), no_cache: args.no_cache, }; - let rootfs = cache.prepare(&request, &ecr::NoProgress)?; + let rootfs = cache.prepare(&request, &progress)?; // Branch based on --kernel flag // Option> (require_equals: the value must use =PATH syntax @@ -63,7 +68,7 @@ fn main() -> Result<()> { } None => { veprintln!("QEMU mode: downloading default kernel..."); - kernel::get_default_kernel(cache.dir(), &arch, &ecr::NoProgress)? + kernel::get_default_kernel(cache.dir(), &arch, &progress)? } }; @@ -81,7 +86,7 @@ fn main() -> Result<()> { arch: arch.clone(), command, }, - &ecr::NoProgress, + &progress, ); // Cleanup happens automatically via tempfile diff --git a/crates/ecr-cli/src/progress.rs b/crates/ecr-cli/src/progress.rs new file mode 100644 index 0000000..f46afa9 --- /dev/null +++ b/crates/ecr-cli/src/progress.rs @@ -0,0 +1,91 @@ +//! Terminal rendering of library progress events (indicatif-backed). +//! +//! This is CLI-owned UI: the library only emits [`Progress`] events, +//! and everything drawn on the terminal lives here. indicatif hides +//! all output when stderr is not a terminal. + +use ecr::progress::{Progress, Unit}; +use indicatif::{ProgressBar, ProgressStyle}; +use std::sync::Mutex; + +/// Renders sequential library operations as bars and spinners on stderr. +pub struct TermProgress { + bar: Mutex>, +} + +impl TermProgress { + pub fn new() -> Self { + TermProgress { + bar: Mutex::new(None), + } + } +} + +impl Default for TermProgress { + fn default() -> Self { + Self::new() + } +} + +impl Drop for TermProgress { + fn drop(&mut self) { + self.finish(); + } +} + +impl Progress for TermProgress { + fn begin(&self, label: &str, unit: Unit, total: Option) { + let mut slot = self.bar.lock().unwrap(); + // The library's `begin` implicitly ends the previous operation. + if let Some(prev) = slot.take() { + prev.finish_and_clear(); + } + + let bar = match (unit, total) { + (Unit::Bytes, Some(total)) => { + let pb = ProgressBar::new(total); + pb.set_style( + ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {msg} {bytes}/{total_bytes} ({eta})") + .unwrap() + .progress_chars("#>-"), + ); + pb.set_message(label.to_string()); + pb + } + (Unit::Bytes, None) => { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg} ({bytes})") + .unwrap(), + ); + pb.set_message(label.to_string()); + pb + } + (Unit::Files, _) => { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg} ({pos} files)") + .unwrap(), + ); + pb.set_message(label.to_string()); + pb + } + }; + *slot = Some(bar); + } + + fn advance(&self, n: u64) { + if let Some(bar) = &*self.bar.lock().unwrap() { + bar.inc(n); + } + } + + fn finish(&self) { + if let Some(bar) = self.bar.lock().unwrap().take() { + bar.finish_and_clear(); + } + } +}