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