Skip to main content

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