pbars/
lib.rs

1use better_term::{Color, flush_styles};
2#[cfg(feature="crossterm")]
3use crossterm::{cursor, execute};
4#[cfg(feature="crossterm")]
5use std::io::stdout;
6
7/// The Type of bar to be used
8#[derive(Debug, Clone)]
9pub enum BarType {
10    /// creates a bar that looks like "████████████████████"
11    Bar,    // [██████████]
12    /// creates a bar that looks like "████████████████████"
13    RawBar, // ██████
14    /// Cycles through ., .., and ...
15    Dots,   // ...
16    /// Cycles through |, /, -, and \
17    Line,   // |
18}
19
20/// The struct for handling progress bars
21///
22/// # Example:
23/// ```rust
24/// use pbars::{PBar, BarType};
25/// use std::thread::sleep;
26/// use std::time::Duration;
27///
28/// fn main() {
29///     // using crossterm, this will create a pbar at 0,0
30///     // without crossterm, this is the only way to create a bar
31///     let mut pbar = PBar::new(BarType::Bar, true, true, 20);
32///
33///     for x in 0..1000 {
34///         // get the percentage complete as a decimal
35///         let percentage_decimal = x as f32 / 1000.0;
36///         // scale the percentage from 0..1 to 0..100 and convert to a u8
37///         let percent = (percentage_decimal * 100.0) as u8;
38///         // update the pbar
39///         pbar.update(percent);
40///         // draw the pbar
41///         pbar.draw();
42///         // delay for 10ms, making this run in 10 seconds
43///         sleep(Duration::from_millis(10));
44///     }
45/// }
46/// ```
47///
48/// # crossterm specific creation
49/// Creating at a location
50/// ```rust
51/// use pbars::{PBar, BarType};
52///
53/// PBar::new_at(0, 0, BarType::Bar, true, true, 20);
54/// ```
55///
56/// Creating at the cursor's current location
57/// ```rust
58/// use pbars::{PBar, BarType};
59///
60/// PBar::new_at_cursor(BarType::Bar, true, true, 20);
61/// ```
62#[derive(Debug, Clone)]
63pub struct PBar {
64    // crossterm position handling
65    #[cfg(feature="crossterm")]
66    x: u16,
67    #[cfg(feature="crossterm")]
68    y: u16,
69
70    // settings
71    color: bool,
72    bar_type: BarType,
73    show_percent: bool,
74    bar_length: u16,
75
76    // for special types
77    repeat_at: u8,
78
79    // value the bar is at
80    percent: u8,
81}
82
83impl PBar {
84    /// create a new progress bar when not using crossterm
85    /// # parameters
86    /// bar_type: the type of bar to display
87    /// color: if it should output in color
88    /// show_percent: if it should show the percentage next to the bar
89    /// bar_length: How long the bar should be
90    #[cfg(not(feature="crossterm"))]
91    pub fn new(bar_type: BarType, color: bool, show_percent: bool, bar_length: u16) -> Self {
92        Self {
93            percent: 0,
94            color, bar_type, bar_length, show_percent,
95            repeat_at: 0,
96        }
97    }
98
99    #[cfg(not(feature="crossterm"))]
100    fn set_pos(&mut self) {
101        print!("\r");
102    }
103
104    /// create a new progress bar when using crossterm
105    /// # parameters
106    /// bar_type: the type of bar to display
107    /// color: if it should output in color
108    /// show_percent: if it should show the percentage next to the bar
109    /// bar_length: How long the bar should be
110    #[cfg(feature="crossterm")]
111    pub fn new(bar_type: BarType, color: bool, show_percent: bool, bar_length: u16) -> Self {
112        Self::new_at(0, 0, bar_type, color, show_percent, bar_length)
113    }
114
115    /// Create a new bar using crossterm at a location
116    /// # parameters
117    /// x: the x location to put the bar (0 is the left)
118    /// y: the y location to put the bar (0 is the top)
119    /// bar_type: the type of bar to display
120    /// color: if it should output in color
121    /// show_percent: if it should show the percentage next to the bar
122    /// bar_length: How long the bar should be
123    #[cfg(feature="crossterm")]
124    pub fn new_at(x: u16, y: u16, bar_type: BarType, color: bool, show_percent: bool, bar_length: u16) -> Self {
125        Self {
126            x, y, percent: 0,
127            color, bar_type, bar_length, show_percent,
128            repeat_at: 0,
129        }
130    }
131
132    /// Creates a new bar using crossterm at the cursor's current position
133    /// # parameters
134    /// bar_type: the type of bar to display
135    /// color: if it should output in color
136    /// show_percent: if it should show the percentage next to the bar
137    /// bar_length: How long the bar should be
138    #[cfg(feature="crossterm")]
139    pub fn new_at_cursor(bar_type: BarType, color: bool, show_percent: bool, bar_length: u16) -> Result<Self, String> {
140        let pos = crossterm::cursor::position();
141        if pos.is_err() {
142            return Err(pos.unwrap_err().to_string());
143        }
144
145        let (x, y) = pos.unwrap();
146
147        Ok(Self::new_at(x, y, bar_type, color, show_percent, bar_length))
148    }
149
150    #[cfg(feature="crossterm")]
151    fn set_pos(&mut self) -> Result<(), String> {
152        let r = execute!(stdout(), cursor::MoveTo(self.x, self.y));
153        if r.is_err() {
154            return Err(r.unwrap_err().to_string());
155        }
156        Ok(())
157    }
158
159    /// Update the bar's progress
160    /// # Parameters
161    /// percent: the new percentage (0 to 100)
162    pub fn update(&mut self, mut percent: u8) {
163        if percent > 100 {
164            percent = 100;
165        }
166        self.percent = percent;
167    }
168
169    fn draw_dots_and_line(&mut self, output: String, color: Color) -> String {
170        format!("{}{}", output, if self.show_percent {
171            if self.color {
172                format!(" {}{}%", color, self.percent)
173            } else {
174                format!(" {}%", self.percent)
175            }
176        } else {
177            format!("")
178        })
179    }
180
181    /// Draw the bar at its set location
182    pub fn draw(&mut self) {
183        #[cfg(feature="crossterm")]
184        {
185            let r = self.set_pos();
186            if r.is_err() {
187                panic!("Failed to set cursor position!");
188            }
189        }
190
191        #[cfg(not(feature="crossterm"))]
192        self.set_pos();
193
194        // get the current completion color of the bar
195        // only used if self.color is true
196        let red = 255 - ((self.percent as f32 / 100.0) * 200.0) as u8;
197        let green = (self.percent as f32 / 100.0 * 200.0) as u8;
198        let color = Color::RGB(red, green, 25);
199
200        let output = match self.bar_type {
201            BarType::RawBar => {
202                let chunk_weight = 100 / self.bar_length;
203                // set how complete the bar should be
204                let bar_completion = self.percent as usize / chunk_weight as usize;
205
206                if self.color {
207                    // create the colored bar
208                    let bar = format!("{}{}", color, "█".repeat(bar_completion));
209
210                    format!("{}{}", bar, if self.show_percent {
211                        format!(" {}{}{}%", color, self.percent, Color::White)
212                    } else { format!("") })
213                } else {
214                    // create the bar
215                    let bar = format!("{}", "█".repeat(bar_completion));
216
217                    format!("{}{}", bar, if self.show_percent {
218                        format!(" {}%", self.percent)
219                    } else { format!("") })
220                }
221            }
222            BarType::Bar => {
223                let chunk_weight = 100 / self.bar_length;
224                // set how complete the bar should be
225                let bar_completion = self.percent as usize / chunk_weight as usize;
226                let mut bar_uncomplete = (100 - (self.percent)) as usize / chunk_weight as usize;
227                // handle if the bar needs to be resized because of rounding issues
228                let add = bar_completion + bar_uncomplete;
229                if add < self.bar_length as usize {
230                    bar_uncomplete += 1;
231                }
232                if add > self.bar_length as usize {
233                    bar_uncomplete -= 1;
234                }
235
236                if self.color {
237                    // create the main bar
238                    let completed_bar = format!("{}{}", color, "█".repeat(bar_completion));
239                    let uncompleted_bar = format!("{}{}", Color::BrightBlack, "█".repeat(bar_uncomplete));
240
241                    // format the bar and return it
242                    format!("{dc}[{}{}{dc}]{}", completed_bar, uncompleted_bar, if self.show_percent {
243                        format!(" {}{}{}%", color, self.percent, Color::White)
244                    } else { format!("") }, dc = Color::White)
245                } else {
246                    // create the main bar
247                    let completed_bar = format!("{}", "█".repeat(bar_completion));
248                    let uncompleted_bar = format!("{}", "=".repeat(bar_uncomplete));
249
250                    // format the bar and return it
251                    format!("[{}{}]{}", completed_bar, uncompleted_bar, if self.show_percent {
252                        format!(" {}%", self.percent)
253                    } else { format!("") })
254                }
255            }
256            BarType::Dots => {
257                if self.percent % 2 == 0 {
258                    self.repeat_at += 1;
259                    if self.repeat_at == 3 {
260                        self.repeat_at = 0;
261                    }
262                }
263
264                let output = match self.repeat_at {
265                    0 => {
266                        format!(".  ")
267                    }
268                    1 => {
269                        format!(".. ")
270                    }
271                    _  => {
272                        format!("...")
273                    }
274                };
275
276                self.draw_dots_and_line(output, color)
277            }
278            BarType::Line => {
279                if self.percent % 2 == 0 {
280                    self.repeat_at += 1;
281                    if self.repeat_at == 4 {
282                        self.repeat_at = 0;
283                    }
284                }
285
286                let output = match self.repeat_at {
287                    0 => {
288                        format!("|")
289                    }
290                    1 => {
291                        format!("/")
292                    }
293                    2 => {
294                        format!("-")
295                    }
296                    _ => {
297                        format!("\\")
298                    }
299                };
300
301                self.draw_dots_and_line(output, color)
302            }
303        };
304
305        #[cfg(feature="crossterm")]
306        print!("{}", output);
307        #[cfg(not(feature="crossterm"))]
308        print!("\r{}", output);
309
310        if self.color {
311            flush_styles()
312        }
313    }
314
315    pub fn is_complete(&self) -> bool {
316        self.percent == 100
317    }
318}
319
320/// Hides the cursor using crossterm
321#[cfg(feature="crossterm")]
322pub fn hide_cursor() {
323    execute!(stdout(), crossterm::cursor::Hide).expect("Failed to hide cursor!");
324}
325
326/// Shows the cursor using crossterm
327#[cfg(feature="crossterm")]
328pub fn show_cursor() {
329    execute!(stdout(), crossterm::cursor::Show).expect("Failed to show cursor!");
330}
331
332impl Default for PBar {
333    /// Creates a bar using default values
334    #[cfg(feature="crossterm")]
335    fn default() -> Self {
336        Self::new_at_cursor(BarType::Bar, true, true, 20).expect("Failed to make default PBar")
337    }
338
339    /// Creates a bar using default values
340    #[cfg(not(feature="crossterm"))]
341    fn default() -> Self {
342        Self::new(BarType::Bar, true, true, 20)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use std::thread;
349    use std::time::Duration;
350    use crossterm::execute;
351    use crossterm::terminal::ClearType;
352    use crate::{BarType, PBar};
353    use crate::{hide_cursor, show_cursor};
354
355    #[test]
356    fn t1() {
357        use std::io::stdout;
358        execute!(stdout(), crossterm::terminal::Clear(ClearType::All)).expect("Failed to clear screen!");
359        let mut pbar = PBar::new_at(0, 1, BarType::RawBar,
360                                    true, true, 20);
361        let mut pbar2 = PBar::new_at(0, 3, BarType::Bar,
362                                     true, true, 20);
363        let mut pbar3 = PBar::new_at(7, 5, BarType::Dots,
364                                     true, true, 20);
365        let mut pbar4 = PBar::new_at(8, 7, BarType::Line,
366                                     true, true, 20);
367
368        hide_cursor();
369
370        execute!(stdout(), crossterm::cursor::MoveTo(0, 5)).expect("Failed to move!");
371        print!("Loading");
372
373        execute!(stdout(), crossterm::cursor::MoveTo(0, 7)).expect("Failed to move!");
374        print!("Loading");
375
376        let max = 1000;
377        for x in 0..max {
378            let percent = ((x as f32 / (max - 1) as f32) * 100.0) as u8;
379            pbar.update(percent);
380            pbar.draw();
381
382            pbar2.update(percent);
383            pbar2.draw();
384
385            pbar3.update(percent);
386            pbar3.draw();
387
388            pbar4.update(percent);
389            pbar4.draw();
390            thread::sleep(Duration::from_millis(10));
391        }
392        println!();
393        show_cursor();
394    }
395}