1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
use std::io::{self, Write};
use crossterm::{QueueableCommand, cursor, ExecutableCommand};

pub struct LoadingBar {
    total: u32,
    current: u32,
    width: u16,
    avg: Option<f32>,
    buffer: String,
}

impl LoadingBar {
    pub fn new(total: u32) -> Self {
        Self {
            total,
            current: 0,
            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
            avg: None,
            buffer: " ".repeat(crossterm::terminal::size().unwrap_or((2, 0)).0 as usize -2),
        }
    }

    #[inline]
    pub fn update(&mut self, elapsed_s: f32, increment: u32) -> &mut Self {
        let clamped = increment.clamp(0, self.total - self.current);
        self.current += clamped;
        self.avg = match self.avg {
            Some(avg) => Some((avg + clamped as f32 / elapsed_s) / 2.0),
            None => Some(clamped as f32 / elapsed_s),
        }; self
    }

    // draws the loading bar and also returns whether the loading bar is full
    #[inline]
    pub fn draw(&mut self, description: &str) -> bool {
        let mut stdout = io::stdout();

        let mul = self.current as f32 / self.total as f32; // percentage multiplier
        let percent = mul * 100.0;
        let eta = (self.total - self.current) as f32 / self.avg.unwrap_or(1.0);

        let loaded = "#".repeat((mul * self.width as f32) as usize);
        let unloaded = " ".repeat(self.width as usize - loaded.len());

        // wipe the lines that were previously drawn
        print!("{}\n{}{}\n{}{}\n{}", self.buffer, cursor::MoveToColumn(0), self.buffer, cursor::MoveToColumn(0), self.buffer, cursor::MoveToColumn(0));
        stdout
            .queue(cursor::Hide).unwrap()
            .queue(cursor::MoveUp(3)).unwrap()
            .queue(cursor::MoveToColumn(0)).unwrap();

        // draw the loading bar & additional info
        write!(
            stdout,
            "\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{}",
            cursor::MoveToColumn(0),
            cursor::MoveToColumn(0),
            self.current,
            self.total,
            match self.avg {
                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)),
                None => String::new(),
            },
        ).unwrap();
        let _ = stdout.flush(); // so that it appears immediately

        // check if the loading bar is full
        if self.current == self.total {
            let _ = stdout.execute(cursor::Show);
            return true;
        }

        stdout // reset for the next draw
            .queue(cursor::MoveUp(2)).unwrap()
            .queue(cursor::MoveToColumn(0)).unwrap();
        false
    }

    /// Gets the time unit that is most appropriate for the given time in seconds
    #[inline]
    pub fn right_time_unit(seconds: f32) -> String {
        if seconds < 60.0 {
            format!("{:.2}s", seconds)
        } else if seconds < 3600.0 {
            format!("{:.2}min", seconds / 60.0)
        } else {
            format!("{:.2}h", seconds / 3600.0)
        }
    }

    /// Gets the apropriate display of the average speed
    #[inline]
    pub fn right_avg_unit(avg: f32) -> String {
        if avg < 1.0 {
            format!("{}/i", Self::right_time_unit(1.0 / avg))
        } else {
            format!("{:.2}i/s", avg)
        }
    }
}