cargo_component_core/
progress.rs

1//! Module for the implementation of a progress bar.
2//!
3//! This is heavily influenced by the `cargo` implementation so
4//! that it has the same appearance.
5
6use crate::terminal::{Terminal, Verbosity};
7use anyhow::Result;
8use owo_colors::OwoColorize;
9use std::io::{stderr, Write};
10use std::time::{Duration, Instant};
11use std::{cmp, fmt};
12use unicode_width::UnicodeWidthChar;
13
14fn is_ci() -> bool {
15    std::env::var("CI").is_ok() || std::env::var("TF_BUILD").is_ok()
16}
17
18/// A progress bar implementation.
19pub struct ProgressBar<'a> {
20    state: Option<State<'a>>,
21}
22
23/// Indicates the style of information for displaying the amount of progress.
24///
25/// See also [`ProgressBar::print_now`] for displaying progress without a bar.
26pub enum ProgressStyle {
27    /// Displays progress as a percentage.
28    ///
29    /// Example: `Fetch [=====================>   ]  88.15%`
30    ///
31    /// This is good for large values like number of bytes downloaded.
32    Percentage,
33    /// Displays progress as a ratio.
34    ///
35    /// Example: `Building [===>                      ] 35/222`
36    ///
37    /// This is good for smaller values where the exact number is useful to see.
38    Ratio,
39    /// Does not display an exact value of how far along it is.
40    ///
41    /// Example: `Fetch [===========>                     ]`
42    ///
43    /// This is good for situations where the exact value is an approximation,
44    /// and thus there isn't anything accurate to display to the user.
45    Indeterminate,
46}
47
48struct Throttle {
49    first: bool,
50    last_update: Instant,
51}
52
53struct State<'a> {
54    terminal: &'a Terminal,
55    format: Format,
56    name: String,
57    done: bool,
58    throttle: Throttle,
59    last_line: Option<String>,
60}
61
62struct Format {
63    style: ProgressStyle,
64    max_width: usize,
65    max_print: usize,
66}
67
68impl<'a> ProgressBar<'a> {
69    /// Creates a new progress bar.
70    ///
71    /// The first parameter is the text displayed to the left of the bar, such
72    /// as "Fetching".
73    ///
74    /// The progress bar is not displayed until explicitly updated with one if
75    /// its methods.
76    ///
77    /// The progress bar may be created in a disabled state if the user has
78    /// disabled progress display (such as with quiet verbosity).
79    pub fn with_style(name: &str, style: ProgressStyle, terminal: &'a Terminal) -> Self {
80        // report no progress when -q (for quiet) or TERM=dumb are set
81        // or if running on Continuous Integration service like Travis where the
82        // output logs get mangled.
83        let dumb = match std::env::var("TERM") {
84            Ok(term) => term == "dumb",
85            Err(_) => false,
86        };
87
88        let verbosity = terminal.verbosity();
89        if verbosity == Verbosity::Quiet || dumb || is_ci() {
90            return Self { state: None };
91        }
92
93        Self::new_priv(name, style, terminal)
94    }
95
96    fn new_priv(name: &str, style: ProgressStyle, terminal: &'a Terminal) -> Self {
97        let width = terminal.width();
98
99        Self {
100            state: width.map(|n| State {
101                terminal,
102                format: Format {
103                    style,
104                    max_width: n,
105                    // 50 gives some space for text after the progress bar,
106                    // even on narrow (e.g. 80 char) terminals.
107                    max_print: 50,
108                },
109                name: name.to_string(),
110                done: false,
111                throttle: Throttle::new(),
112                last_line: None,
113            }),
114        }
115    }
116
117    /// Disables the progress bar, ensuring it won't be displayed.
118    pub fn disable(&mut self) {
119        self.state = None;
120    }
121
122    /// Returns whether or not the progress bar is allowed to be displayed.
123    pub fn is_enabled(&self) -> bool {
124        self.state.is_some()
125    }
126
127    /// Creates a new `Progress` with the [`ProgressStyle::Percentage`] style.
128    ///
129    /// See [`ProgressBar::with_style`] for more information.
130    pub fn new(name: &str, terminal: &'a Terminal) -> Self {
131        Self::with_style(name, ProgressStyle::Percentage, terminal)
132    }
133
134    /// Updates the state of the progress bar.
135    ///
136    /// * `cur` should be how far along the progress is.
137    /// * `max` is the maximum value for the progress bar.
138    /// * `msg` is a small piece of text to display at the end of the progress
139    ///   bar. It will be truncated with `...` if it does not fit on the
140    ///   terminal.
141    ///
142    /// This may not actually update the display if `tick` is being called too
143    /// quickly.
144    pub fn tick(&mut self, cur: usize, max: usize, msg: &str) -> Result<()> {
145        let s = match &mut self.state {
146            Some(s) => s,
147            None => return Ok(()),
148        };
149
150        // Don't update too often as it can cause excessive performance loss
151        // just putting stuff onto the terminal. We also want to avoid
152        // flickering by not drawing anything that goes away too quickly. As a
153        // result we've got two branches here:
154        //
155        // 1. If we haven't drawn anything, we wait for a period of time to
156        //    actually start drawing to the console. This ensures that
157        //    short-lived operations don't flicker on the console. Currently
158        //    there's a 500ms delay to when we first draw something.
159        // 2. If we've drawn something, then we rate limit ourselves to only
160        //    draw to the console every so often. Currently there's a 100ms
161        //    delay between updates.
162        if !s.throttle.allowed() {
163            return Ok(());
164        }
165
166        s.tick(cur, max, msg)
167    }
168
169    /// Updates the state of the progress bar.
170    ///
171    /// This is the same as [`ProgressBar::tick`], but ignores rate throttling
172    /// and forces the display to be updated immediately.
173    ///
174    /// This may be useful for situations where you know you aren't calling
175    /// `tick` too fast, and accurate information is more important than
176    /// limiting the console update rate.
177    pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> Result<()> {
178        match self.state {
179            Some(ref mut s) => s.tick(cur, max, msg),
180            None => Ok(()),
181        }
182    }
183
184    /// Returns whether or not updates are currently being throttled.
185    ///
186    /// This can be useful if computing the values for calling the
187    /// [`ProgressBar::tick`] function may require some expensive work.
188    pub fn update_allowed(&mut self) -> bool {
189        match &mut self.state {
190            Some(s) => s.throttle.allowed(),
191            None => false,
192        }
193    }
194
195    /// Displays progress without a bar.
196    ///
197    /// The given `msg` is the text to display after the status message.
198    ///
199    /// Example: `Downloading 61 crates, remaining bytes: 28.0 MB`
200    ///
201    /// This does not have any rate limit throttling, so be careful about
202    /// calling it too often.
203    pub fn print_now(&mut self, msg: &str) -> Result<()> {
204        match &mut self.state {
205            Some(s) => s.print("", msg),
206            None => Ok(()),
207        }
208    }
209
210    /// Clears the progress bar from the console.
211    pub fn clear(&mut self) {
212        if let Some(ref mut s) = self.state {
213            s.clear();
214        }
215    }
216}
217
218impl Throttle {
219    fn new() -> Throttle {
220        Throttle {
221            first: true,
222            last_update: Instant::now(),
223        }
224    }
225
226    fn allowed(&mut self) -> bool {
227        if self.first {
228            let delay = Duration::from_millis(500);
229            if self.last_update.elapsed() < delay {
230                return false;
231            }
232        } else {
233            let interval = Duration::from_millis(100);
234            if self.last_update.elapsed() < interval {
235                return false;
236            }
237        }
238        self.update();
239        true
240    }
241
242    fn update(&mut self) {
243        self.first = false;
244        self.last_update = Instant::now();
245    }
246}
247
248impl<'a> State<'a> {
249    fn tick(&mut self, cur: usize, max: usize, msg: &str) -> Result<()> {
250        if self.done {
251            return Ok(());
252        }
253
254        if max > 0 && cur == max {
255            self.done = true;
256        }
257
258        // Write out a pretty header, then the progress bar itself, and then
259        // return back to the beginning of the line for the next print.
260        self.try_update_max_width();
261        if let Some(pbar) = self.format.progress(cur, max) {
262            self.print(&pbar, msg)?;
263        }
264        Ok(())
265    }
266
267    fn print(&mut self, prefix: &str, msg: &str) -> Result<()> {
268        self.throttle.update();
269        self.try_update_max_width();
270
271        // make sure we have enough room for the header
272        if self.format.max_width < 15 {
273            return Ok(());
274        }
275
276        let mut line = prefix.to_string();
277        self.format.render(&mut line, msg);
278        while line.len() < self.format.max_width - 15 {
279            line.push(' ');
280        }
281
282        let mut state = self.terminal.state_mut();
283
284        // Only update if the line has changed.
285        if !state.needs_clear || self.last_line.as_ref() != Some(&line) {
286            let name_cyan = self.name.cyan();
287
288            let status = if state.output.supports_color() {
289                &name_cyan as &dyn fmt::Display
290            } else {
291                &self.name
292            };
293
294            state.output.print(status, None, true)?;
295            write!(&mut stderr(), "{line}\r")?;
296            self.last_line = Some(line);
297            state.needs_clear = true;
298        }
299
300        Ok(())
301    }
302
303    fn clear(&mut self) {
304        // No need to clear if the progress is not currently being displayed.
305        if self.last_line.is_some() {
306            self.terminal.state_mut().clear_stderr();
307            self.last_line = None;
308        }
309    }
310
311    fn try_update_max_width(&mut self) {
312        if let Some(width) = self.terminal.width() {
313            self.format.max_width = width;
314        }
315    }
316}
317
318impl Format {
319    fn progress(&self, cur: usize, max: usize) -> Option<String> {
320        assert!(cur <= max);
321        // Render the percentage at the far right and then figure how long the
322        // progress bar is
323        let pct = (cur as f64) / (max as f64);
324        let pct = if !pct.is_finite() { 0.0 } else { pct };
325        let stats = match self.style {
326            ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
327            ProgressStyle::Ratio => format!(" {}/{}", cur, max),
328            ProgressStyle::Indeterminate => String::new(),
329        };
330        let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
331        let display_width = match self.width().checked_sub(extra_len) {
332            Some(n) => n,
333            None => return None,
334        };
335
336        let mut string = String::with_capacity(self.max_width);
337        string.push('[');
338        let hashes = display_width as f64 * pct;
339        let hashes = hashes as usize;
340
341        // Draw the `===>`
342        if hashes > 0 {
343            for _ in 0..hashes - 1 {
344                string.push('=');
345            }
346            if cur == max {
347                string.push('=');
348            } else {
349                string.push('>');
350            }
351        }
352
353        // Draw the empty space we have left to do
354        for _ in 0..(display_width - hashes) {
355            string.push(' ');
356        }
357        string.push(']');
358        string.push_str(&stats);
359
360        Some(string)
361    }
362
363    fn render(&self, string: &mut String, msg: &str) {
364        let mut avail_msg_len = self.max_width - string.len() - 15;
365        let mut ellipsis_pos = 0;
366        if avail_msg_len <= 3 {
367            return;
368        }
369        for c in msg.chars() {
370            let display_width = c.width().unwrap_or(0);
371            if avail_msg_len >= display_width {
372                avail_msg_len -= display_width;
373                string.push(c);
374                if avail_msg_len >= 3 {
375                    ellipsis_pos = string.len();
376                }
377            } else {
378                string.truncate(ellipsis_pos);
379                string.push_str("...");
380                break;
381            }
382        }
383    }
384
385    #[cfg(test)]
386    fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
387        let mut ret = self.progress(cur, max)?;
388        self.render(&mut ret, msg);
389        Some(ret)
390    }
391
392    fn width(&self) -> usize {
393        cmp::min(self.max_width, self.max_print)
394    }
395}
396
397impl<'a> Drop for State<'a> {
398    fn drop(&mut self) {
399        self.clear();
400    }
401}
402
403#[cfg(test)]
404mod test {
405    use super::*;
406
407    #[test]
408    fn test_progress_status() {
409        let format = Format {
410            style: ProgressStyle::Ratio,
411            max_print: 40,
412            max_width: 60,
413        };
414        assert_eq!(
415            format.progress_status(0, 4, ""),
416            Some("[                   ] 0/4".to_string())
417        );
418        assert_eq!(
419            format.progress_status(1, 4, ""),
420            Some("[===>               ] 1/4".to_string())
421        );
422        assert_eq!(
423            format.progress_status(2, 4, ""),
424            Some("[========>          ] 2/4".to_string())
425        );
426        assert_eq!(
427            format.progress_status(3, 4, ""),
428            Some("[=============>     ] 3/4".to_string())
429        );
430        assert_eq!(
431            format.progress_status(4, 4, ""),
432            Some("[===================] 4/4".to_string())
433        );
434
435        assert_eq!(
436            format.progress_status(3999, 4000, ""),
437            Some("[===========> ] 3999/4000".to_string())
438        );
439        assert_eq!(
440            format.progress_status(4000, 4000, ""),
441            Some("[=============] 4000/4000".to_string())
442        );
443
444        assert_eq!(
445            format.progress_status(3, 4, ": short message"),
446            Some("[=============>     ] 3/4: short message".to_string())
447        );
448        assert_eq!(
449            format.progress_status(3, 4, ": msg thats just fit"),
450            Some("[=============>     ] 3/4: msg thats just fit".to_string())
451        );
452        assert_eq!(
453            format.progress_status(3, 4, ": msg that's just fit"),
454            Some("[=============>     ] 3/4: msg that's just...".to_string())
455        );
456
457        // combining diacritics have width zero and thus can fit max_width.
458        let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
459        assert_eq!(
460            format.progress_status(3, 4, zalgo_msg),
461            Some("[=============>     ] 3/4".to_string() + zalgo_msg)
462        );
463
464        // some non-ASCII ellipsize test
465        assert_eq!(
466            format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
467            Some("[=============>     ] 3/4_123456789123456e\u{301}\u{301}...".to_string())
468        );
469        assert_eq!(
470            format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
471            Some("[=============>     ] 3/4:每個漢字佔據了...".to_string())
472        );
473        assert_eq!(
474            // handle breaking at middle of character
475            format.progress_status(3, 4, ":-每個漢字佔據了兩個字元"),
476            Some("[=============>     ] 3/4:-每個漢字佔據了...".to_string())
477        );
478    }
479
480    #[test]
481    fn test_progress_status_percentage() {
482        let format = Format {
483            style: ProgressStyle::Percentage,
484            max_print: 40,
485            max_width: 60,
486        };
487        assert_eq!(
488            format.progress_status(0, 77, ""),
489            Some("[               ]   0.00%".to_string())
490        );
491        assert_eq!(
492            format.progress_status(1, 77, ""),
493            Some("[               ]   1.30%".to_string())
494        );
495        assert_eq!(
496            format.progress_status(76, 77, ""),
497            Some("[=============> ]  98.70%".to_string())
498        );
499        assert_eq!(
500            format.progress_status(77, 77, ""),
501            Some("[===============] 100.00%".to_string())
502        );
503    }
504
505    #[test]
506    fn test_progress_status_too_short() {
507        let format = Format {
508            style: ProgressStyle::Percentage,
509            max_print: 25,
510            max_width: 25,
511        };
512        assert_eq!(
513            format.progress_status(1, 1, ""),
514            Some("[] 100.00%".to_string())
515        );
516
517        let format = Format {
518            style: ProgressStyle::Percentage,
519            max_print: 24,
520            max_width: 24,
521        };
522        assert_eq!(format.progress_status(1, 1, ""), None);
523    }
524}