prog_rs/
progress.rs

1//! Defines a basic progress bar that needs to be manually updated.
2
3use std::boxed::Box;
4use std::cmp::min;
5use std::io;
6use std::io::prelude::*;
7use std::time::{Duration, Instant};
8
9//   ____             __ _
10//  / ___|___  _ __  / _(_) __ _
11// | |   / _ \| '_ \| |_| |/ _` |
12// | |__| (_) | | | |  _| | (_| |
13//  \____\___/|_| |_|_| |_|\__, |
14//                         |___/
15
16/// Different display modes for the progress.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum BarPosition {
19    /// The progress bar will be floating right, with extra informations on
20    /// left of it.
21    Right,
22
23    /// The progress bar will be displayed on left, just after the prefix.
24    Left,
25}
26
27/// Available streams to display in.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum OutputStream {
30    /// Standart output.
31    StdOut,
32
33    /// Standart Error.
34    StdErr,
35}
36
37impl OutputStream {
38    fn get(self) -> Box<dyn Write> {
39        use OutputStream::*;
40        match self {
41            StdOut => Box::new(io::stdout()),
42            StdErr => Box::new(io::stderr()),
43        }
44    }
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48struct ProgressConfig {
49    bar_position: BarPosition,
50    bar_width: usize,
51    display_width: Option<usize>,
52    extra_infos: String,
53    output_stream: OutputStream,
54    prefix: String,
55    refresh_delay: Duration,
56    shape_body: char,
57    shape_head: char,
58    shape_void: char,
59}
60
61impl Default for ProgressConfig {
62    fn default() -> Self {
63        Self {
64            bar_position: BarPosition::Left,
65            bar_width: 40,
66            display_width: None,
67            extra_infos: String::new(),
68            output_stream: OutputStream::StdOut,
69            prefix: String::new(),
70            refresh_delay: Duration::from_millis(200),
71            shape_body: '=',
72            shape_head: '>',
73            shape_void: ' ',
74        }
75    }
76}
77
78//  ____
79// |  _ \ _ __ ___   __ _ _ __ ___  ___ ___
80// | |_) | '__/ _ \ / _` | '__/ _ \/ __/ __|
81// |  __/| | | (_) | (_| | | |  __/\__ \__ \
82// |_|   |_|  \___/ \__, |_|  \___||___/___/
83//                  |___/
84
85/// A generic progress bar that needs to be manually updated.
86///
87/// # Example
88///
89/// ```
90/// let mut progress = Progress::new()
91///     .with_bar_width(30)
92///     .with_extra_infos("Hello, World!")
93///     .with_refresh_delay(Duration::from_millis(100))
94///     .with_output_stream(OutputStream::StdErr);
95///
96/// for i in 0..10_000 {
97///     progress.update(i as f32 / 10_000.).unwrap();
98///     progress = progress
99///         .with_extra_infos(format!("Hello, World! ({}/10000)", i + 1));
100///     sleep(Duration::from_nanos(110));
101/// }
102///
103/// progress.finished().ok();
104/// ```
105#[derive(Clone, Debug)]
106pub struct Progress {
107    config: ProgressConfig,
108    last_update_time: Option<Instant>,
109}
110
111impl<'a> Progress {
112    /// Create a new progress bar with default display settings.
113    pub fn new() -> Self {
114        Self {
115            config: ProgressConfig::default(),
116            last_update_time: None,
117        }
118    }
119
120    /// Update extra informations displayed next to the progress bar.
121    pub fn set_extra_infos<S>(&mut self, extra_infos: S)
122    where
123        S: Into<String>,
124    {
125        self.config.extra_infos = extra_infos.into()
126    }
127
128    /// Check if the timer specified by `with_refresh_delay` has decayed.
129    pub fn need_refresh(&self) -> bool {
130        if let Some(last_update_time) = self.last_update_time {
131            return last_update_time.elapsed() >= self.config.refresh_delay;
132        }
133        true
134    }
135
136    fn bar_shape(&self, progress: f32) -> (usize, usize, usize) {
137        let body_length = min(
138            self.config.bar_width + 1,
139            (progress * (self.config.bar_width + 1) as f32).round() as usize,
140        );
141        let mut void_length = (self.config.bar_width + 1) - body_length;
142        let mut head_length = 0;
143
144        if void_length > 0 {
145            void_length -= 1;
146            head_length += 1;
147        }
148
149        (body_length, void_length, head_length)
150    }
151
152    /// Redraw the progress bar if the timer has decayed.
153    pub fn update(&mut self, progress: f32) -> io::Result<()> {
154        if !self.need_refresh() {
155            return Ok(());
156        }
157
158        self.last_update_time = Some(Instant::now());
159
160        let (body, void, head) = self.bar_shape(progress);
161        let body = self.config.shape_body.to_string().repeat(body);
162        let head = self.config.shape_head.to_string().repeat(head);
163        let void = self.config.shape_void.to_string().repeat(void);
164
165        // Compute display shape
166        let required_width =
167            self.config.bar_width + self.config.prefix.len() + self.config.extra_infos.len() + 13;
168        let display_width = self
169            .config
170            .display_width
171            .unwrap_or_else(|| term_size::dimensions_stdout().map(|(w, _)| w).unwrap_or(80));
172
173        let (prefix, padding) = {
174            if display_width >= required_width {
175                (
176                    &self.config.prefix[..],
177                    " ".repeat(display_width - required_width),
178                )
179            } else if self.config.prefix.len() >= required_width - display_width {
180                let prefix_len = self.config.prefix.len() - (required_width - display_width);
181                (&self.config.prefix[0..prefix_len], String::new())
182            } else {
183                ("", String::new())
184            }
185        };
186
187        let text = match self.config.bar_position {
188            BarPosition::Left => format!(
189                "\r{} {:>5.1}% [{}{}{}] {}{}",
190                prefix,
191                100. * progress,
192                body,
193                head,
194                void,
195                self.config.extra_infos,
196                padding
197            ),
198            BarPosition::Right => format!(
199                "\r{} {}{} [{}{}{}] {:>5.1}%",
200                prefix,
201                padding,
202                self.config.extra_infos,
203                body,
204                head,
205                void,
206                100. * progress
207            ),
208        };
209
210        // Display text
211        let mut stream = self.config.output_stream.get();
212        stream.write_all(&text.as_bytes())?;
213        stream.flush()
214    }
215
216    /// Redraw the progress bar for the last time.
217    pub fn finished(&mut self) -> io::Result<()> {
218        self.last_update_time = None;
219        self.update(1.0)?;
220        writeln!(&mut self.config.output_stream.get())
221    }
222}
223
224impl<'a> Default for Progress {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230// __        ___ _   _       ____
231// \ \      / (_) |_| |__   |  _ \ _ __ ___   __ _ _ __ ___  ___ ___
232//  \ \ /\ / /| | __| '_ \  | |_) | '__/ _ \ / _` | '__/ _ \/ __/ __|
233//   \ V  V / | | |_| | | | |  __/| | | (_) | (_| | | |  __/\__ \__ \
234//    \_/\_/  |_|\__|_| |_| |_|   |_|  \___/ \__, |_|  \___||___/___/
235//                                           |___/
236
237/// A type that contains a progress bar that can be updated using `with_{parameter}` syntax.
238pub trait WithProgress: Sized {
239    fn get_progress(&mut self) -> &mut Progress;
240
241    /// Specify a progress bar to use, which allows to copy configuration.
242    ///
243    /// # Example
244    ///
245    /// ```
246    /// use prog_rs::prelude::*;
247    /// use prog_rs::{OutputStream, Progress};
248    ///
249    /// let progress = Progress::new()
250    ///     .with_bar_width(50)
251    ///     .with_output_stream(OutputStream::StdErr);
252    ///
253    /// for i in (0..100).progress().with_progress(progress.clone()) {
254    ///     do_something(i);
255    /// }
256    ///
257    /// for i in (0..100).progress().with_progress(progress) {
258    ///     do_something(i);
259    /// }
260    /// ```
261    fn with_progress(mut self, progress: Progress) -> Self {
262        *self.get_progress() = progress;
263        self
264    }
265
266    /// Change the style of the bar disposition.
267    fn with_bar_position(mut self, bar_position: BarPosition) -> Self {
268        self.get_progress().config.bar_position = bar_position;
269        self
270    }
271
272    /// Change the width of the progress bar.
273    fn with_bar_width(mut self, bar_width: usize) -> Self {
274        self.get_progress().config.bar_width = bar_width;
275        self
276    }
277
278    /// Change the width of the text the displayed informations should try to
279    /// fit in. The terminal width will be detected by default.
280    fn with_display_width(mut self, display_width: usize) -> Self {
281        self.get_progress().config.display_width = Some(display_width);
282        self
283    }
284
285    /// Specify extra informations to display.
286    fn with_extra_infos<S>(mut self, extra_infos: S) -> Self
287    where
288        S: Into<String>,
289    {
290        self.get_progress().config.extra_infos = extra_infos.into();
291        self
292    }
293
294    /// Change the output stream the progress bar is displayed in. By default
295    /// standart output is used.
296    fn with_output_stream(mut self, output_stream: OutputStream) -> Self {
297        self.get_progress().config.output_stream = output_stream;
298        self
299    }
300
301    /// Change the text displayed in front of progress informations.
302    ///
303    /// # Example
304    ///
305    /// ```
306    /// use prog_rs::prelude::*;
307    ///
308    /// for i in (0..1000)
309    ///     .progress()
310    ///     .with_prefix("Computing something ...")
311    /// {
312    ///     do_something(i);
313    /// }
314    fn with_prefix<S>(mut self, prefix: S) -> Self
315    where
316        S: Into<String>,
317    {
318        self.get_progress().config.prefix = prefix.into();
319        self
320    }
321
322    /// Change the minimum delay between two display updates.
323    fn with_refresh_delay(mut self, refresh_delay: Duration) -> Self {
324        self.get_progress().config.refresh_delay = refresh_delay;
325        self
326    }
327
328    /// Change the character used to draw the body of the progress bar.
329    fn with_shape_body(mut self, shape_body: char) -> Self {
330        self.get_progress().config.shape_body = shape_body;
331        self
332    }
333
334    /// Change the character used to draw the head of the progress bar.
335    fn with_shape_head(mut self, shape_head: char) -> Self {
336        self.get_progress().config.shape_head = shape_head;
337        self
338    }
339
340    /// Change the character used to draw the background of the progress bar.
341    fn with_shape_void(mut self, shape_void: char) -> Self {
342        self.get_progress().config.shape_void = shape_void;
343        self
344    }
345}
346
347impl WithProgress for Progress {
348    fn get_progress(&mut self) -> &mut Progress {
349        self
350    }
351}