1use std::io::{IsTerminal, Write};
7use std::sync::Mutex;
8
9const WIDTH: usize = 20;
11
12const LINE: usize = 72;
14
15pub struct Bar {
16 total: usize,
17 done: Option<Mutex<usize>>,
20}
21
22impl Bar {
23 pub fn new(total: usize) -> Bar {
24 let bar = Bar {
25 total,
26 done: std::io::stderr().is_terminal().then(|| Mutex::new(0)),
27 };
28 if bar.done.is_some() {
29 draw(&line(0, total, ""));
30 }
31 bar
32 }
33
34 pub fn done(&self, label: &str) {
36 let Some(done) = &self.done else { return };
37 let mut done = done.lock().unwrap();
38 *done += 1;
39 draw(&line(*done, self.total, label));
40 }
41
42 pub fn finish(&self) {
44 if self.done.is_some() {
45 draw("");
46 }
47 }
48}
49
50fn draw(text: &str) {
52 let mut err = std::io::stderr().lock();
53 let _ = write!(err, "\r{text}\x1b[K");
54 let _ = err.flush();
55}
56
57fn line(done: usize, total: usize, label: &str) -> String {
59 let filled = (done * WIDTH).checked_div(total).unwrap_or(WIDTH);
60 let counter = format!("{done}/{total}");
61 let bar = format!(
62 "[{}{}] {counter}",
63 "#".repeat(filled),
64 "-".repeat(WIDTH - filled)
65 );
66 if label.is_empty() {
67 return bar;
68 }
69 format!(
70 "{bar} {}",
71 crate::report::truncate(label, LINE.saturating_sub(bar.len() + 1))
72 )
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn the_bar_fills_with_the_count() {
81 assert_eq!(line(0, 4, ""), "[--------------------] 0/4");
82 assert_eq!(line(1, 4, "a"), "[#####---------------] 1/4 a");
83 assert_eq!(line(4, 4, "d"), "[####################] 4/4 d");
84 }
85
86 #[test]
89 fn a_long_label_is_cut_to_the_line() {
90 let long = "x".repeat(200);
91 assert_eq!(line(7, 95, &long).len(), LINE);
92 }
93
94 #[test]
95 fn nothing_to_scan_is_a_full_bar() {
96 assert_eq!(line(0, 0, ""), "[####################] 0/0");
97 }
98}