use console::Term;
use exarch_core::ProgressCallback;
use indicatif::ProgressBar;
use indicatif::ProgressState;
use indicatif::ProgressStyle;
use std::fmt::Write;
use std::path::Path;
pub struct CliProgress {
bar: ProgressBar,
bytes_written: u64,
}
impl CliProgress {
#[must_use]
pub fn new(total: usize, message: &str) -> Self {
let bar = ProgressBar::new(total as u64);
bar.set_style(
ProgressStyle::default_bar()
.template(
"{msg} [{bar:40.cyan/blue}] {pos}/{len} files ({bytes}, {bytes_per_sec}, {eta})",
)
.unwrap_or_else(|_| ProgressStyle::default_bar())
.with_key("bytes", |state: &ProgressState, w: &mut dyn Write| {
write!(w, "{}", humanize_bytes(state.pos())).unwrap_or(());
})
.with_key("bytes_per_sec", |state: &ProgressState, w: &mut dyn Write| {
let per_sec = state.per_sec();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let bytes_per_sec = per_sec as u64;
write!(w, "{}/s", humanize_bytes(bytes_per_sec)).unwrap_or(());
})
.with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
let eta = state.eta();
write!(w, "{}", humanize_duration(eta)).unwrap_or(());
})
.progress_chars("█▓░"),
);
bar.set_message(message.to_string());
Self {
bar,
bytes_written: 0,
}
}
#[must_use]
pub fn should_show() -> bool {
Term::stdout().is_term()
}
}
impl Drop for CliProgress {
fn drop(&mut self) {
self.bar.finish_and_clear();
}
}
impl ProgressCallback for CliProgress {
fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
}
fn on_bytes_written(&mut self, bytes: u64) {
self.bytes_written += bytes;
self.bar.set_position(self.bytes_written);
}
fn on_entry_complete(&mut self, _path: &Path) {
self.bar.inc(1);
}
fn on_complete(&mut self) {
self.bar.finish_and_clear();
}
}
fn humanize_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
if bytes >= TB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
fn humanize_duration(duration: std::time::Duration) -> String {
let secs = duration.as_secs();
if secs >= 3600 {
format!("{}h{}m", secs / 3600, (secs % 3600) / 60)
} else if secs >= 60 {
format!("{}m{}s", secs / 60, secs % 60)
} else {
format!("{secs}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_humanize_bytes() {
assert_eq!(humanize_bytes(0), "0 B");
assert_eq!(humanize_bytes(512), "512 B");
assert_eq!(humanize_bytes(1024), "1.0 KB");
assert_eq!(humanize_bytes(1536), "1.5 KB");
assert_eq!(humanize_bytes(1024 * 1024), "1.0 MB");
assert_eq!(humanize_bytes(1024 * 1024 * 1024), "1.0 GB");
assert_eq!(humanize_bytes(1024_u64.pow(4)), "1.0 TB");
}
#[test]
fn test_humanize_duration() {
assert_eq!(humanize_duration(std::time::Duration::from_secs(0)), "0s");
assert_eq!(humanize_duration(std::time::Duration::from_secs(30)), "30s");
assert_eq!(
humanize_duration(std::time::Duration::from_secs(90)),
"1m30s"
);
assert_eq!(
humanize_duration(std::time::Duration::from_secs(3661)),
"1h1m"
);
}
#[test]
fn test_progress_callback() {
let mut progress = CliProgress::new(100, "Testing");
progress.on_entry_start(Path::new("test.txt"), 100, 1);
progress.on_bytes_written(1024);
progress.on_entry_complete(Path::new("test.txt"));
assert_eq!(progress.bytes_written, 1024);
}
}