use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::sync::{Arc, Mutex};
use zesven::progress::ProgressReporter;
pub struct CliProgress {
multi: MultiProgress,
overall: ProgressBar,
current: Arc<Mutex<Option<ProgressBar>>>,
quiet: bool,
}
impl CliProgress {
pub fn new(total_entries: u64, quiet: bool) -> Self {
let multi = MultiProgress::new();
let overall = if quiet {
ProgressBar::hidden()
} else {
let pb = multi.add(ProgressBar::new(total_entries));
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} files ({eta})")
.unwrap()
.progress_chars("#>-"),
);
pb
};
Self {
multi,
overall,
current: Arc::new(Mutex::new(None)),
quiet,
}
}
pub fn set_message(&self, msg: impl Into<String>) {
if !self.quiet {
self.overall.set_message(msg.into());
}
}
#[allow(dead_code)] pub fn inc(&self, delta: u64) {
self.overall.inc(delta);
}
pub fn finish(&self) {
self.overall.finish_with_message("Done");
}
pub fn finish_with_message(&self, msg: impl Into<String>) {
self.overall.finish_with_message(msg.into());
}
#[allow(dead_code)] pub fn create_spinner(&self, message: &str) -> ProgressBar {
if self.quiet {
return ProgressBar::hidden();
}
let pb = self.multi.add(ProgressBar::new_spinner());
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(100));
pb
}
}
impl ProgressReporter for CliProgress {
fn on_entry_start(&mut self, entry_name: &str, entry_size: u64) {
if self.quiet {
return;
}
let pb = self.multi.add(ProgressBar::new(entry_size));
pb.set_style(
ProgressStyle::default_bar()
.template(" {spinner:.green} {wide_msg} [{bar:30}] {bytes}/{total_bytes}")
.unwrap()
.progress_chars("#>-"),
);
let display_name = if entry_name.len() > 40 {
format!("...{}", &entry_name[entry_name.len() - 37..])
} else {
entry_name.to_string()
};
pb.set_message(display_name);
*self.current.lock().unwrap() = Some(pb);
}
fn on_progress(&mut self, bytes_extracted: u64, _total_bytes: u64) -> bool {
if let Some(pb) = self.current.lock().unwrap().as_ref() {
pb.set_position(bytes_extracted);
}
true }
fn on_entry_complete(&mut self, _entry_name: &str, success: bool) {
if let Some(pb) = self.current.lock().unwrap().take() {
if success {
pb.finish_and_clear();
} else {
pb.abandon_with_message("Error");
}
}
self.overall.inc(1);
}
}
#[derive(Clone)]
pub struct WriteProgress {
bar: ProgressBar,
in_hand: Arc<Mutex<Vec<String>>>,
quiet: bool,
}
impl WriteProgress {
pub fn new(total_entries: u64, quiet: bool) -> Self {
let bar = if quiet {
ProgressBar::hidden()
} else {
let pb = ProgressBar::new(total_entries);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:30.cyan/blue}] {pos}/{len} {wide_msg}",
)
.unwrap()
.progress_chars("#>-"),
);
pb
};
Self {
bar,
in_hand: Arc::new(Mutex::new(Vec::new())),
quiet,
}
}
pub fn finish(&self) {
self.bar.finish_and_clear();
}
pub fn finish_with_message(&self, msg: &str) {
self.bar.abandon_with_message(msg.to_string());
}
fn describe(&self) {
if self.quiet {
return;
}
let Ok(names) = self.in_hand.lock() else {
return;
};
let message = match names.len() {
0 => String::new(),
1 => format!("compressing {}", shorten(&names[0])),
n => format!("compressing {n} entries"),
};
self.bar.set_message(message);
}
}
fn shorten(name: &str) -> String {
if name.chars().count() > 40 {
let tail: String = name.chars().skip(name.chars().count() - 37).collect();
format!("...{tail}")
} else {
name.to_string()
}
}
impl ProgressReporter for WriteProgress {
fn on_entry_start(&mut self, entry_name: &str, _size: u64) {
if let Ok(mut names) = self.in_hand.lock() {
names.push(entry_name.to_string());
}
self.describe();
}
fn on_progress(&mut self, produced: u64, declared: u64) -> bool {
if self.quiet {
return true;
}
let held = self.in_hand.lock();
if let Some(name) = held.as_ref().ok().and_then(|names| names.first()) {
self.bar.set_message(format!(
"compressing {} - {} written of about {}",
shorten(name),
indicatif::HumanBytes(produced),
indicatif::HumanBytes(declared),
));
}
true
}
fn on_entry_complete(&mut self, entry_name: &str, _success: bool) {
if let Ok(mut names) = self.in_hand.lock() {
names.retain(|held| held != entry_name);
}
self.bar.inc(1);
self.describe();
}
}
pub struct SimpleProgress {
bar: ProgressBar,
}
impl SimpleProgress {
pub fn new(total: u64, quiet: bool) -> Self {
let bar = if quiet {
ProgressBar::hidden()
} else {
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
pb
};
Self { bar }
}
#[allow(dead_code)] pub fn set_position(&self, pos: u64) {
self.bar.set_position(pos);
}
pub fn set_message(&self, msg: impl Into<String>) {
self.bar.set_message(msg.into());
}
pub fn finish(&self) {
self.bar.finish_and_clear();
}
pub fn finish_with_message(&self, msg: impl Into<String>) {
self.bar.finish_with_message(msg.into());
}
}
#[cfg(test)]
mod tests {
use super::WriteProgress;
use zesven::progress::ProgressReporter;
#[test]
fn test_the_bar_follows_what_has_been_written() {
let mut bar = WriteProgress::new(3, true);
bar.on_entry_start("a.bin", 10);
bar.on_entry_start("b.bin", 10);
bar.on_entry_start("c.bin", 10);
assert_eq!(
bar.bar.position(),
0,
"announcing a batch moved the bar before anything was written",
);
bar.on_entry_complete("a.bin", true);
assert_eq!(bar.bar.position(), 1);
bar.on_entry_complete("b.bin", true);
bar.on_entry_complete("c.bin", true);
assert_eq!(bar.bar.position(), 3);
}
#[test]
fn test_the_message_tracks_what_is_being_worked_on() {
let mut bar = WriteProgress::new(2, true);
bar.on_entry_start("first.bin", 10);
bar.on_entry_start("second.bin", 10);
assert_eq!(bar.in_hand.lock().expect("held").len(), 2);
bar.on_entry_complete("first.bin", true);
let held = bar.in_hand.lock().expect("held");
assert_eq!(*held, vec!["second.bin".to_string()]);
}
}