counter_cli/
loading_bar.rs

1use std::io::{self, Write};
2use crossterm::{QueueableCommand, cursor, ExecutableCommand};
3
4pub struct LoadingBar {
5    total: u32,
6    current: u32,
7    width: u16,
8    avg: Option<f32>,
9    buffer: String,
10}
11
12impl LoadingBar {
13    pub fn new(total: u32) -> Self {
14        Self {
15            total,
16            current: 0,
17            width: ((crossterm::terminal::size().unwrap_or((2, 0)).0 as f32 - 2.0) * 0.56) as u16, // loading bar is 56% of the terminal width
18            avg: None,
19            buffer: " ".repeat(crossterm::terminal::size().unwrap_or((2, 0)).0 as usize -2),
20        }
21    }
22
23    #[inline]
24    pub fn update(&mut self, elapsed_s: f32, increment: u32) -> &mut Self {
25        let clamped = increment.clamp(0, self.total - self.current);
26        self.current += clamped;
27        self.avg = match self.avg {
28            Some(avg) => Some((avg + clamped as f32 / elapsed_s) / 2.0),
29            None => Some(clamped as f32 / elapsed_s),
30        }; self
31    }
32
33    // draws the loading bar and also returns whether the loading bar is full
34    #[inline]
35    pub fn draw(&mut self, description: &str) -> bool {
36        let mut stdout = io::stdout();
37
38        let mul = self.current as f32 / self.total as f32; // percentage multiplier
39        let percent = mul * 100.0;
40        let eta = (self.total - self.current) as f32 / self.avg.unwrap_or(1.0);
41
42        let loaded = "#".repeat((mul * self.width as f32) as usize);
43        let unloaded = " ".repeat(self.width as usize - loaded.len());
44
45        // wipe the lines that were previously drawn
46        print!("{}\n{}{}\n{}{}\n{}", self.buffer, cursor::MoveToColumn(0), self.buffer, cursor::MoveToColumn(0), self.buffer, cursor::MoveToColumn(0));
47        stdout
48            .queue(cursor::Hide).unwrap()
49            .queue(cursor::MoveUp(3)).unwrap()
50            .queue(cursor::MoveToColumn(0)).unwrap();
51
52        // draw the loading bar & additional info
53        write!(
54            stdout,
55            "\x1b[36;1minfo: \x1b[0m{description}...\n{}\x1b[34m> [\x1b[32m{loaded}{unloaded}\x1b[34m]\n{}> | \x1b[33m{percent:.2}% \x1b[34m| \x1b[33m{}\x1b[34m/\x1b[33m{} \x1b[34m|\x1b[0m{}",
56            cursor::MoveToColumn(0),
57            cursor::MoveToColumn(0),
58            self.current,
59            self.total,
60            match self.avg {
61                Some(avg) => format!(" \x1b[33m{} \x1b[34m| \x1b[36meta\x1b[34m: \x1b[33m{} \x1b[34m|\x1b[0m", Self::right_avg_unit(avg), Self::right_time_unit(eta)),
62                None => String::new(),
63            },
64        ).unwrap();
65        let _ = stdout.flush(); // so that it appears immediately
66
67        // check if the loading bar is full
68        if self.current == self.total {
69            let _ = stdout.execute(cursor::Show);
70            return true;
71        }
72
73        stdout // reset for the next draw
74            .queue(cursor::MoveUp(2)).unwrap()
75            .queue(cursor::MoveToColumn(0)).unwrap();
76        false
77    }
78
79    /// Gets the time unit that is most appropriate for the given time in seconds
80    #[inline]
81    pub fn right_time_unit(seconds: f32) -> String {
82        if seconds < 60.0 {
83            format!("{:.2}s", seconds)
84        } else if seconds < 3600.0 {
85            format!("{:.2}min", seconds / 60.0)
86        } else {
87            format!("{:.2}h", seconds / 3600.0)
88        }
89    }
90
91    /// Gets the apropriate display of the average speed
92    #[inline]
93    pub fn right_avg_unit(avg: f32) -> String {
94        if avg < 1.0 {
95            format!("{}/i", Self::right_time_unit(1.0 / avg))
96        } else {
97            format!("{:.2}i/s", avg)
98        }
99    }
100}