cli_animate/
progress.rs

1use crate::utils::Color;
2use std::default::Default;
3use std::io::{Stdout, Write};
4use std::sync::{Arc, Mutex};
5
6/// A `ProgressBar` handles the animation of a progress bar.
7pub struct ProgressBar {
8    /// The start value of the progress.
9    /// The `start` and `goal` values must be absolute values, not relative to each other.
10    /// For example, if your ideal state is 700 and your current state is 500, set `start` to 500 and `goal` to 700.
11    /// Vice versa, if you want to start at 50% of the progress, set `start` to 50 and `goal` to 100.
12    pub start: u64,
13
14    /// The goal value of the progress. It must be an absolute value, not relative to the start.
15    pub goal: u64,
16
17    /// A closure to get the current progress value.
18    pub get_progress: Arc<Mutex<dyn Fn() -> u64 + Send>>,
19
20    pub style: Style,
21}
22
23/// A `Style` defines the appearance of a progress bar.
24pub struct Style {
25    /// The character used to display the progress bar, such as `=`, `#`, `*`, etc.
26    pub bar_character: char,
27
28    /// The length of the progress bar in characters.
29    pub bar_length: u64,
30
31    /// The color of the progress bar. It will be printed as a 24 bit color.
32    pub color: Color,
33}
34
35impl Default for Style {
36    fn default() -> Self {
37        Style {
38            bar_character: '=',
39            bar_length: 50,
40            color: Color::White,
41        }
42    }
43}
44
45pub struct StyleBuilder {
46    bar_character: Option<char>,
47    bar_length: Option<u64>,
48    color: Option<Color>,
49}
50
51impl StyleBuilder {
52    pub fn new() -> StyleBuilder {
53        StyleBuilder {
54            bar_character: None,
55            bar_length: None,
56            color: None,
57        }
58    }
59
60    pub fn bar_character(mut self, character: char) -> StyleBuilder {
61        self.bar_character = Some(character);
62        self
63    }
64
65    pub fn bar_length(mut self, length: u64) -> StyleBuilder {
66        self.bar_length = Some(length);
67        self
68    }
69
70    pub fn color(mut self, color: Color) -> StyleBuilder {
71        self.color = Some(color);
72        self
73    }
74
75    pub fn build(self) -> Style {
76        Style {
77            bar_character: self.bar_character.unwrap_or('='),
78            bar_length: self.bar_length.unwrap_or(50),
79            color: self.color.unwrap_or(Color::White),
80        }
81    }
82}
83
84impl ProgressBar {
85    /// `new()` initializes a new progress bar.
86    pub fn new<F>(start: u64, goal: u64, get_progress: F, style: Style) -> ProgressBar
87    where
88        F: Fn() -> u64 + Send + 'static,
89    {
90        assert!(start <= goal);
91
92        ProgressBar {
93            start,
94            goal,
95            get_progress: Arc::new(Mutex::new(get_progress)),
96            style,
97        }
98    }
99
100    /// `start()` starts the animation of the progress bar.
101    /// It displays from 0% and goes to 100%.
102    pub fn start(&self, writer: &mut Stdout) {
103        let mut current_value = self.start;
104
105        println!("\x1B[?25l"); // x1B[?25l hides cursor.
106        while current_value < self.goal {
107            current_value = (self.get_progress.lock().unwrap())();
108            self.update_display(writer, current_value);
109        }
110
111        write!(writer, "\n\x1b[0m").unwrap(); // x1B[0m resets color.
112    }
113
114    // NOTE: This function is separated from `start()` just to make it testable.
115    // It's impossible to test output to stdout, so we test only this function.
116    fn update_display(&self, writer: &mut dyn Write, current_value: u64) {
117        let percentage =
118            ((current_value - self.start) as f64 / (self.goal - self.start) as f64) * 100.0;
119
120        let bar_length = self.style.bar_length as usize;
121        let completed = ((percentage / 100.0) * bar_length as f64) as usize;
122        let bar = self.style.bar_character.to_string().repeat(completed)
123            + &" ".repeat(bar_length - completed);
124
125        let color_code = self.style.color.to_ansi_code();
126
127        write!(writer, "\r{}[{}]", &color_code, bar).unwrap();
128
129        writer.flush().unwrap();
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use std::io::Cursor;
137
138    #[test]
139    fn test_update_progress_success() {
140        let progress_bar = ProgressBar::new(0, 100, || 0, Style::default());
141        let mut writer = Cursor::new(Vec::new());
142
143        progress_bar.update_display(&mut writer, 50);
144
145        let expected_output = "\r\x1b[37m[=========================                         ]"; // 25 =, 25 ' '.
146        assert_eq!(writer.get_ref().as_slice(), expected_output.as_bytes());
147    }
148}