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.
This commit is contained in:
2026-10-03 00:06:22 +02:00
parent 1cfcf0e433
commit b3ad0ab3e0
4 changed files with 103 additions and 3 deletions
+3
View File
@@ -21,3 +21,6 @@ clap = { version = "4", features = ["derive", "env"] }
# Error handling
anyhow = "1"
# Progress bar rendering of library progress events
indicatif = "0.18"
+8 -3
View File
@@ -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<Option<PathBuf>> (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
+91
View File
@@ -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<Option<ProgressBar>>,
}
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<u64>) {
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();
}
}
}