1use 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
12pub struct Progress<'gctx> {
30 state: Option<State<'gctx>>,
31}
32
33pub enum ProgressStyle {
37 Percentage,
43 Ratio,
49 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 pub fn with_style(
91 name: &str,
92 style: ProgressStyle,
93 gctx: &'gctx GlobalContext,
94 ) -> Progress<'gctx> {
95 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 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 pub fn disable(&mut self) {
141 self.state = None;
142 }
143
144 pub fn is_enabled(&self) -> bool {
146 self.state.is_some()
147 }
148
149 pub fn new(name: &str, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
153 Self::with_style(name, ProgressStyle::Percentage, gctx)
154 }
155
156 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 if !s.throttle.allowed() {
184 return Ok(());
185 }
186
187 s.tick(cur, max, msg)
188 }
189
190 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 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 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 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 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 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 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 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 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 + 15 ;
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 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 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 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 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 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}