cargo/util/
progress.rs

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