cargo/util/
progress.rs

1use std::cmp;
2use std::env;
3use std::time::{Duration, Instant};
4
5use crate::core::shell::Verbosity;
6use crate::util::{is_ci, CargoResult, Config};
7
8use unicode_width::UnicodeWidthChar;
9
10pub struct Progress<'cfg> {
11    state: Option<State<'cfg>>,
12}
13
14pub enum ProgressStyle {
15    Percentage,
16    Ratio,
17}
18
19struct Throttle {
20    first: bool,
21    last_update: Instant,
22}
23
24struct State<'cfg> {
25    config: &'cfg Config,
26    format: Format,
27    name: String,
28    done: bool,
29    throttle: Throttle,
30    last_line: Option<String>,
31}
32
33struct Format {
34    style: ProgressStyle,
35    max_width: usize,
36    max_print: usize,
37}
38
39impl<'cfg> Progress<'cfg> {
40    pub fn with_style(name: &str, style: ProgressStyle, cfg: &'cfg Config) -> Progress<'cfg> {
41        // report no progress when -q (for quiet) or TERM=dumb are set
42        // or if running on Continuous Integration service like Travis where the
43        // output logs get mangled.
44        let dumb = match env::var("TERM") {
45            Ok(term) => term == "dumb",
46            Err(_) => false,
47        };
48        if cfg.shell().verbosity() == Verbosity::Quiet || dumb || is_ci() {
49            return Progress { state: None };
50        }
51
52        Progress {
53            state: cfg.shell().err_width().map(|n| State {
54                config: cfg,
55                format: Format {
56                    style,
57                    max_width: n,
58                    max_print: 80,
59                },
60                name: name.to_string(),
61                done: false,
62                throttle: Throttle::new(),
63                last_line: None,
64            }),
65        }
66    }
67
68    pub fn disable(&mut self) {
69        self.state = None;
70    }
71
72    pub fn is_enabled(&self) -> bool {
73        self.state.is_some()
74    }
75
76    pub fn new(name: &str, cfg: &'cfg Config) -> Progress<'cfg> {
77        Self::with_style(name, ProgressStyle::Percentage, cfg)
78    }
79
80    pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
81        let s = match &mut self.state {
82            Some(s) => s,
83            None => return Ok(()),
84        };
85
86        // Don't update too often as it can cause excessive performance loss
87        // just putting stuff onto the terminal. We also want to avoid
88        // flickering by not drawing anything that goes away too quickly. As a
89        // result we've got two branches here:
90        //
91        // 1. If we haven't drawn anything, we wait for a period of time to
92        //    actually start drawing to the console. This ensures that
93        //    short-lived operations don't flicker on the console. Currently
94        //    there's a 500ms delay to when we first draw something.
95        // 2. If we've drawn something, then we rate limit ourselves to only
96        //    draw to the console every so often. Currently there's a 100ms
97        //    delay between updates.
98        if !s.throttle.allowed() {
99            return Ok(());
100        }
101
102        s.tick(cur, max, "")
103    }
104
105    pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
106        match self.state {
107            Some(ref mut s) => s.tick(cur, max, msg),
108            None => Ok(()),
109        }
110    }
111
112    pub fn update_allowed(&mut self) -> bool {
113        match &mut self.state {
114            Some(s) => s.throttle.allowed(),
115            None => false,
116        }
117    }
118
119    pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
120        match &mut self.state {
121            Some(s) => s.print("", msg),
122            None => Ok(()),
123        }
124    }
125
126    pub fn clear(&mut self) {
127        if let Some(ref mut s) = self.state {
128            s.clear();
129        }
130    }
131}
132
133impl Throttle {
134    fn new() -> Throttle {
135        Throttle {
136            first: true,
137            last_update: Instant::now(),
138        }
139    }
140
141    fn allowed(&mut self) -> bool {
142        if self.first {
143            let delay = Duration::from_millis(500);
144            if self.last_update.elapsed() < delay {
145                return false;
146            }
147        } else {
148            let interval = Duration::from_millis(100);
149            if self.last_update.elapsed() < interval {
150                return false;
151            }
152        }
153        self.update();
154        true
155    }
156
157    fn update(&mut self) {
158        self.first = false;
159        self.last_update = Instant::now();
160    }
161}
162
163impl<'cfg> State<'cfg> {
164    fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
165        if self.done {
166            return Ok(());
167        }
168
169        if max > 0 && cur == max {
170            self.done = true;
171        }
172
173        // Write out a pretty header, then the progress bar itself, and then
174        // return back to the beginning of the line for the next print.
175        self.try_update_max_width();
176        if let Some(pbar) = self.format.progress(cur, max) {
177            self.print(&pbar, msg)?;
178        }
179        Ok(())
180    }
181
182    fn print(&mut self, prefix: &str, msg: &str) -> CargoResult<()> {
183        self.throttle.update();
184        self.try_update_max_width();
185
186        // make sure we have enough room for the header
187        if self.format.max_width < 15 {
188            return Ok(());
189        }
190
191        let mut line = prefix.to_string();
192        self.format.render(&mut line, msg);
193        while line.len() < self.format.max_width - 15 {
194            line.push(' ');
195        }
196
197        // Only update if the line has changed.
198        if self.config.shell().is_cleared() || self.last_line.as_ref() != Some(&line) {
199            let mut shell = self.config.shell();
200            shell.set_needs_clear(false);
201            shell.status_header(&self.name)?;
202            write!(shell.err(), "{}\r", line)?;
203            self.last_line = Some(line);
204            shell.set_needs_clear(true);
205        }
206
207        Ok(())
208    }
209
210    fn clear(&mut self) {
211        // No need to clear if the progress is not currently being displayed.
212        if self.last_line.is_some() && !self.config.shell().is_cleared() {
213            self.config.shell().err_erase_line();
214            self.last_line = None;
215        }
216    }
217
218    fn try_update_max_width(&mut self) {
219        if let Some(n) = self.config.shell().err_width() {
220            self.format.max_width = n;
221        }
222    }
223}
224
225impl Format {
226    fn progress(&self, cur: usize, max: usize) -> Option<String> {
227        assert!(cur <= max);
228        // Render the percentage at the far right and then figure how long the
229        // progress bar is
230        let pct = (cur as f64) / (max as f64);
231        let pct = if !pct.is_finite() { 0.0 } else { pct };
232        let stats = match self.style {
233            ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
234            ProgressStyle::Ratio => format!(" {}/{}", cur, max),
235        };
236        let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
237        let display_width = match self.width().checked_sub(extra_len) {
238            Some(n) => n,
239            None => return None,
240        };
241
242        let mut string = String::with_capacity(self.max_width);
243        string.push('[');
244        let hashes = display_width as f64 * pct;
245        let hashes = hashes as usize;
246
247        // Draw the `===>`
248        if hashes > 0 {
249            for _ in 0..hashes - 1 {
250                string.push_str("=");
251            }
252            if cur == max {
253                string.push_str("=");
254            } else {
255                string.push_str(">");
256            }
257        }
258
259        // Draw the empty space we have left to do
260        for _ in 0..(display_width - hashes) {
261            string.push_str(" ");
262        }
263        string.push_str("]");
264        string.push_str(&stats);
265
266        Some(string)
267    }
268
269    fn render(&self, string: &mut String, msg: &str) {
270        let mut avail_msg_len = self.max_width - string.len() - 15;
271        let mut ellipsis_pos = 0;
272        if avail_msg_len <= 3 {
273            return;
274        }
275        for c in msg.chars() {
276            let display_width = c.width().unwrap_or(0);
277            if avail_msg_len >= display_width {
278                avail_msg_len -= display_width;
279                string.push(c);
280                if avail_msg_len >= 3 {
281                    ellipsis_pos = string.len();
282                }
283            } else {
284                string.truncate(ellipsis_pos);
285                string.push_str("...");
286                break;
287            }
288        }
289    }
290
291    #[cfg(test)]
292    fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
293        let mut ret = self.progress(cur, max)?;
294        self.render(&mut ret, msg);
295        Some(ret)
296    }
297
298    fn width(&self) -> usize {
299        cmp::min(self.max_width, self.max_print)
300    }
301}
302
303impl<'cfg> Drop for State<'cfg> {
304    fn drop(&mut self) {
305        self.clear();
306    }
307}
308
309#[test]
310fn test_progress_status() {
311    let format = Format {
312        style: ProgressStyle::Ratio,
313        max_print: 40,
314        max_width: 60,
315    };
316    assert_eq!(
317        format.progress_status(0, 4, ""),
318        Some("[                   ] 0/4".to_string())
319    );
320    assert_eq!(
321        format.progress_status(1, 4, ""),
322        Some("[===>               ] 1/4".to_string())
323    );
324    assert_eq!(
325        format.progress_status(2, 4, ""),
326        Some("[========>          ] 2/4".to_string())
327    );
328    assert_eq!(
329        format.progress_status(3, 4, ""),
330        Some("[=============>     ] 3/4".to_string())
331    );
332    assert_eq!(
333        format.progress_status(4, 4, ""),
334        Some("[===================] 4/4".to_string())
335    );
336
337    assert_eq!(
338        format.progress_status(3999, 4000, ""),
339        Some("[===========> ] 3999/4000".to_string())
340    );
341    assert_eq!(
342        format.progress_status(4000, 4000, ""),
343        Some("[=============] 4000/4000".to_string())
344    );
345
346    assert_eq!(
347        format.progress_status(3, 4, ": short message"),
348        Some("[=============>     ] 3/4: short message".to_string())
349    );
350    assert_eq!(
351        format.progress_status(3, 4, ": msg thats just fit"),
352        Some("[=============>     ] 3/4: msg thats just fit".to_string())
353    );
354    assert_eq!(
355        format.progress_status(3, 4, ": msg that's just fit"),
356        Some("[=============>     ] 3/4: msg that's just...".to_string())
357    );
358
359    // combining diacritics have width zero and thus can fit max_width.
360    let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
361    assert_eq!(
362        format.progress_status(3, 4, zalgo_msg),
363        Some("[=============>     ] 3/4".to_string() + zalgo_msg)
364    );
365
366    // some non-ASCII ellipsize test
367    assert_eq!(
368        format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
369        Some("[=============>     ] 3/4_123456789123456e\u{301}\u{301}...".to_string())
370    );
371    assert_eq!(
372        format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
373        Some("[=============>     ] 3/4:每個漢字佔據了...".to_string())
374    );
375}
376
377#[test]
378fn test_progress_status_percentage() {
379    let format = Format {
380        style: ProgressStyle::Percentage,
381        max_print: 40,
382        max_width: 60,
383    };
384    assert_eq!(
385        format.progress_status(0, 77, ""),
386        Some("[               ]   0.00%".to_string())
387    );
388    assert_eq!(
389        format.progress_status(1, 77, ""),
390        Some("[               ]   1.30%".to_string())
391    );
392    assert_eq!(
393        format.progress_status(76, 77, ""),
394        Some("[=============> ]  98.70%".to_string())
395    );
396    assert_eq!(
397        format.progress_status(77, 77, ""),
398        Some("[===============] 100.00%".to_string())
399    );
400}
401
402#[test]
403fn test_progress_status_too_short() {
404    let format = Format {
405        style: ProgressStyle::Percentage,
406        max_print: 25,
407        max_width: 25,
408    };
409    assert_eq!(
410        format.progress_status(1, 1, ""),
411        Some("[] 100.00%".to_string())
412    );
413
414    let format = Format {
415        style: ProgressStyle::Percentage,
416        max_print: 24,
417        max_width: 24,
418    };
419    assert_eq!(format.progress_status(1, 1, ""), None);
420}