use indicatif::{ProgressBar, ProgressStyle};
use std::io::IsTerminal;
use std::sync::atomic::{AtomicU64, Ordering};
pub struct Progress {
total: u64,
done: AtomicU64,
mode: Mode,
noun: &'static str,
}
enum Mode {
Bar(ProgressBar),
Plain,
Silent,
}
const LOG_INTERVAL: u64 = 25;
impl Progress {
pub fn new(total: u64, silent: bool) -> Self {
let mode = if silent {
Mode::Silent
} else if std::io::stderr().is_terminal() {
let bar = ProgressBar::new(total);
bar.set_style(
ProgressStyle::with_template("{bar:40} {percent}%")
.unwrap()
.progress_chars("=> "),
);
Mode::Bar(bar)
} else {
Mode::Plain
};
Progress {
total,
done: AtomicU64::new(0),
mode,
noun: "images",
}
}
pub fn new_counting(total: u64, silent: bool, noun: &'static str) -> Self {
Progress {
noun,
..Progress::new(total, silent)
}
}
pub fn tick(&self) {
self.tick_by(1);
}
pub fn tick_by(&self, n: u64) {
let before = self.done.fetch_add(n, Ordering::Relaxed);
let after = before + n;
match &self.mode {
Mode::Bar(bar) => bar.set_position(after),
Mode::Plain => {
if after / LOG_INTERVAL != before / LOG_INTERVAL || after == self.total {
eprintln!("{}/{} {} processed", after, self.total, self.noun);
}
}
Mode::Silent => {}
}
}
pub fn println(&self, msg: &str) {
match &self.mode {
Mode::Bar(bar) => bar.println(msg),
Mode::Plain | Mode::Silent => eprintln!("{msg}"),
}
}
pub fn finish(self) {
if let Mode::Bar(bar) = self.mode {
bar.finish_and_clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn silent_mode_tick_does_not_panic() {
let p = Progress::new(10, true);
for _ in 0..10 {
p.tick();
}
p.finish();
}
#[test]
fn silent_mode_println_still_prints() {
let p = Progress::new(5, true);
p.println("an error message");
}
#[test]
fn zero_total_does_not_panic() {
let p = Progress::new(0, true);
p.tick();
p.finish();
}
#[test]
fn silent_mode_tick_by_does_not_panic() {
let p = Progress::new(100, true);
p.tick_by(40);
p.tick_by(60);
p.finish();
}
#[test]
fn a_caller_can_count_something_other_than_images() {
let p = Progress::new_counting(10, true, "coordinates");
assert_eq!(p.noun, "coordinates");
assert_eq!(Progress::new(10, true).noun, "images");
}
#[test]
fn concurrent_tick_from_multiple_threads_reaches_correct_total() {
use std::sync::Arc;
let progress = Arc::new(Progress::new(1000, true));
let handles: Vec<_> = (0..10)
.map(|_| {
let p = Arc::clone(&progress);
std::thread::spawn(move || {
for _ in 0..100 {
p.tick();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(progress.done.load(Ordering::Relaxed), 1000);
}
}
pub fn human_duration(d: std::time::Duration) -> String {
let ms = d.as_millis();
if ms < 1000 {
return format!("{ms}ms");
}
let secs = d.as_secs();
match secs {
0..=9 => {
let s = d.as_millis() as f64 / 1000.0;
format!("{s:.1}s")
}
10..=59 => format!("{secs}s"),
60..=3599 => {
let (m, s) = (secs / 60, secs % 60);
if s == 0 {
format!("{m}m")
} else {
format!("{m}m {s}s")
}
}
_ => {
let (h, m) = (secs / 3600, (secs % 3600) / 60);
if m == 0 {
format!("{h}h")
} else {
format!("{h}h {m}m")
}
}
}
}
pub fn human_duration_ms(ms: u64) -> String {
human_duration(std::time::Duration::from_millis(ms))
}
#[cfg(test)]
mod duration_tests {
use super::{human_duration, human_duration_ms};
use std::time::Duration;
#[test]
fn reads_the_way_a_person_would_say_it() {
let cases = [
(Duration::from_millis(0), "0ms"),
(Duration::from_millis(840), "840ms"),
(Duration::from_millis(1000), "1.0s"),
(Duration::from_millis(3240), "3.2s"),
(Duration::from_secs(41), "41s"),
(Duration::from_secs(59), "59s"),
(Duration::from_secs(60), "1m"),
(Duration::from_secs(95), "1m 35s"),
(Duration::from_secs(3599), "59m 59s"),
(Duration::from_secs(3600), "1h"),
(Duration::from_secs(8040), "2h 14m"),
];
for (d, want) in cases {
assert_eq!(human_duration(d), want, "for {d:?}");
}
}
#[test]
fn the_millisecond_form_matches() {
assert_eq!(human_duration_ms(0), "0ms");
assert_eq!(human_duration_ms(5_412_000), "1h 30m");
}
#[test]
fn no_unit_is_ever_shown_as_zero() {
for secs in [3600, 7200, 60, 120, 600] {
let s = human_duration(Duration::from_secs(secs));
assert!(!s.contains(" 0m") && !s.contains(" 0s"), "{secs}s gave {s}");
}
}
}