use std::io::{IsTerminal, Write};
use std::sync::Mutex;
const WIDTH: usize = 20;
const LINE: usize = 72;
pub struct Bar {
total: usize,
done: Option<Mutex<usize>>,
}
impl Bar {
pub fn new(total: usize) -> Bar {
let bar = Bar {
total,
done: std::io::stderr().is_terminal().then(|| Mutex::new(0)),
};
if bar.done.is_some() {
draw(&line(0, total, ""));
}
bar
}
pub fn done(&self, label: &str) {
let Some(done) = &self.done else { return };
let mut done = done.lock().unwrap();
*done += 1;
draw(&line(*done, self.total, label));
}
pub fn finish(&self) {
if self.done.is_some() {
draw("");
}
}
}
fn draw(text: &str) {
let mut err = std::io::stderr().lock();
let _ = write!(err, "\r{text}\x1b[K");
let _ = err.flush();
}
fn line(done: usize, total: usize, label: &str) -> String {
let filled = (done * WIDTH).checked_div(total).unwrap_or(WIDTH);
let counter = format!("{done}/{total}");
let bar = format!(
"[{}{}] {counter}",
"#".repeat(filled),
"-".repeat(WIDTH - filled)
);
if label.is_empty() {
return bar;
}
format!(
"{bar} {}",
crate::report::truncate(label, LINE.saturating_sub(bar.len() + 1))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_bar_fills_with_the_count() {
assert_eq!(line(0, 4, ""), "[--------------------] 0/4");
assert_eq!(line(1, 4, "a"), "[#####---------------] 1/4 a");
assert_eq!(line(4, 4, "d"), "[####################] 4/4 d");
}
#[test]
fn a_long_label_is_cut_to_the_line() {
let long = "x".repeat(200);
assert_eq!(line(7, 95, &long).len(), LINE);
}
#[test]
fn nothing_to_scan_is_a_full_bar() {
assert_eq!(line(0, 0, ""), "[####################] 0/0");
}
}