1use std::cmp;
4use std::time::{Duration, Instant};
5
6use crate::util::context::ProgressWhen;
7use crate::util::{CargoResult, GlobalContext};
8use anstyle_progress::TermProgress;
9use cargo_util::is_ci;
10use cargo_util_terminal::Shell;
11use cargo_util_terminal::Verbosity;
12use unicode_width::UnicodeWidthChar;
13
14pub struct Progress<'gctx> {
32 gctx: &'gctx GlobalContext,
33 state: Option<State>,
34}
35
36impl<'gctx> Progress<'gctx> {
37 pub fn new(name: &str, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
41 Self::with_style(name, ProgressStyle::Percentage, gctx)
42 }
43
44 pub fn with_style(
55 name: &str,
56 style: ProgressStyle,
57 gctx: &'gctx GlobalContext,
58 ) -> Progress<'gctx> {
59 let dumb = match gctx.get_env("TERM") {
63 Ok(term) => term == "dumb",
64 Err(_) => false,
65 };
66 let progress_config = gctx.progress_config();
67 match progress_config.when {
68 ProgressWhen::Always => return Progress::new_priv(name, style, gctx),
69 ProgressWhen::Never => return Progress { gctx, state: None },
70 ProgressWhen::Auto => {}
71 }
72 if gctx.shell().verbosity() == Verbosity::Quiet || dumb || is_ci() {
73 return Progress { gctx, state: None };
74 }
75 Progress::new_priv(name, style, gctx)
76 }
77
78 fn new_priv(name: &str, style: ProgressStyle, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
79 let progress_config = gctx.progress_config();
80 let width = progress_config
81 .width
82 .or_else(|| gctx.shell().err_width().progress_max_width());
83
84 Progress {
85 gctx,
86 state: width.map(|n| State {
87 format: Format {
88 style,
89 max_width: n,
90 max_print: 50,
93 term_integration: TerminalIntegration::from_config(gctx),
94 unicode: gctx.shell().err_unicode(),
95 },
96 name: name.to_string(),
97 done: false,
98 throttle: Throttle::new(),
99 last_line: None,
100 fixed_width: progress_config.width,
101 }),
102 }
103 }
104
105 pub fn disable(&mut self) {
107 self.state = None;
108 }
109
110 pub fn is_enabled(&self) -> bool {
112 self.state.is_some()
113 }
114
115 pub fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
125 let Some(s) = &mut self.state else {
126 return Ok(());
127 };
128
129 let mut shell = self.gctx.shell();
130
131 if !s.throttle.allowed() {
144 return Ok(());
145 }
146
147 s.tick(cur, max, msg, &mut shell)
148 }
149
150 pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
159 let mut shell = self.gctx.shell();
160
161 match self.state {
162 Some(ref mut s) => s.tick(cur, max, msg, &mut shell),
163 None => Ok(()),
164 }
165 }
166
167 pub fn update_allowed(&mut self) -> bool {
172 match &mut self.state {
173 Some(s) => s.throttle.allowed(),
174 None => false,
175 }
176 }
177
178 pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
187 let mut shell = self.gctx.shell();
188
189 match &mut self.state {
190 Some(s) => s.print(ProgressOutput::PrintNow, msg, &mut shell),
191 None => Ok(()),
192 }
193 }
194
195 pub fn clear(&mut self) {
197 let mut shell = self.gctx.shell();
198
199 if let Some(ref mut s) = self.state {
200 s.clear(&mut shell);
201 }
202 }
203
204 pub fn indicate_error(&mut self) {
206 if let Some(s) = &mut self.state {
207 s.format.term_integration.error()
208 }
209 }
210}
211
212impl<'gctx> Drop for Progress<'gctx> {
213 fn drop(&mut self) {
214 self.clear();
215 }
216}
217
218pub enum ProgressStyle {
222 Percentage,
228 Ratio,
234 Indeterminate,
241}
242
243struct State {
244 format: Format,
245 name: String,
246 done: bool,
247 throttle: Throttle,
248 last_line: Option<String>,
249 fixed_width: Option<usize>,
250}
251
252impl State {
253 fn tick(&mut self, cur: usize, max: usize, msg: &str, shell: &mut Shell) -> CargoResult<()> {
254 if self.done {
255 write!(shell.err(), "{}", self.format.term_integration.remove())?;
256 return Ok(());
257 }
258
259 if max > 0 && cur == max {
260 self.done = true;
261 }
262
263 self.try_update_max_width(shell);
266 if let Some(pbar) = self.format.progress(cur, max) {
267 self.print(pbar, msg, shell)?;
268 }
269 Ok(())
270 }
271
272 fn print(&mut self, progress: ProgressOutput, msg: &str, shell: &mut Shell) -> CargoResult<()> {
273 self.throttle.update();
274 self.try_update_max_width(shell);
275
276 let (mut line, report) = match progress {
277 ProgressOutput::PrintNow => (String::new(), None),
278 ProgressOutput::TextAndReport(prefix, report) => (prefix, Some(report)),
279 ProgressOutput::Report(report) => (String::new(), Some(report)),
280 };
281
282 if self.format.max_width < 15 {
284 if let Some(tb) = report {
286 write!(shell.err(), "{tb}\r")?;
287 }
288 return Ok(());
289 }
290
291 self.format.render(&mut line, msg);
292 while line.len() < self.format.max_width - 15 {
293 line.push(' ');
294 }
295
296 if shell.is_cleared() || self.last_line.as_ref() != Some(&line) {
298 shell.set_needs_clear(false);
299 shell.transient_status(&self.name)?;
300 if let Some(tb) = report {
301 write!(shell.err(), "{line}{tb}\r")?;
302 } else {
303 write!(shell.err(), "{line}\r")?;
304 }
305 self.last_line = Some(line);
306 shell.set_needs_clear(true);
307 }
308
309 Ok(())
310 }
311
312 fn clear(&mut self, shell: &mut Shell) {
313 let _ = write!(shell.err(), "{}", self.format.term_integration.remove());
315 if self.last_line.is_some() && !shell.is_cleared() {
317 shell.err_erase_line();
318 self.last_line = None;
319 }
320 }
321
322 fn try_update_max_width(&mut self, shell: &mut Shell) {
323 if self.fixed_width.is_none() {
324 if let Some(n) = shell.err_width().progress_max_width() {
325 self.format.max_width = n;
326 }
327 }
328 }
329}
330
331struct Format {
332 style: ProgressStyle,
333 max_width: usize,
334 max_print: usize,
335 term_integration: TerminalIntegration,
336 unicode: bool,
337}
338
339impl Format {
340 fn progress(&self, cur: usize, max: usize) -> Option<ProgressOutput> {
341 assert!(cur <= max);
342 let pct = (cur as f64) / (max as f64);
345 let pct = if !pct.is_finite() { 0.0 } else { pct };
346 let stats = match self.style {
347 ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
348 ProgressStyle::Ratio => format!(" {cur}/{max}"),
349 ProgressStyle::Indeterminate => String::new(),
350 };
351 let report = match self.style {
352 ProgressStyle::Percentage | ProgressStyle::Ratio => {
353 let pct = (pct * 100.0) as u8;
354 let pct = pct.clamp(0, 100);
355 self.term_integration.value(pct)
356 }
357 ProgressStyle::Indeterminate => self.term_integration.indeterminate(),
358 };
359
360 let extra_len = stats.len() + 2 + 15 ;
361 let Some(display_width) = self.width().checked_sub(extra_len) else {
362 if self.term_integration.enabled {
363 return Some(ProgressOutput::Report(report));
364 }
365 return None;
366 };
367
368 let mut string = String::with_capacity(self.max_width);
369 string.push('[');
370 let hashes = display_width as f64 * pct;
371 let hashes = hashes as usize;
372
373 if hashes > 0 {
375 for _ in 0..hashes - 1 {
376 string.push('=');
377 }
378 if cur == max {
379 string.push('=');
380 } else {
381 string.push('>');
382 }
383 }
384
385 for _ in 0..(display_width - hashes) {
387 string.push(' ');
388 }
389 string.push(']');
390 string.push_str(&stats);
391
392 Some(ProgressOutput::TextAndReport(string, report))
393 }
394
395 fn render(&self, string: &mut String, msg: &str) {
396 let mut avail_msg_len = self.max_width - string.len() - 15;
397 let mut ellipsis_pos = 0;
398
399 let (ellipsis, ellipsis_width) = if self.unicode { ("…", 1) } else { ("...", 3) };
400
401 if avail_msg_len <= ellipsis_width {
402 return;
403 }
404 for c in msg.chars() {
405 let display_width = c.width().unwrap_or(0);
406 if avail_msg_len >= display_width {
407 avail_msg_len -= display_width;
408 string.push(c);
409 if avail_msg_len >= ellipsis_width {
410 ellipsis_pos = string.len();
411 }
412 } else {
413 string.truncate(ellipsis_pos);
414 string.push_str(ellipsis);
415 break;
416 }
417 }
418 }
419
420 #[cfg(test)]
421 fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
422 let mut ret = match self.progress(cur, max)? {
423 ProgressOutput::TextAndReport(text, _) => text,
425 _ => return None,
426 };
427 self.render(&mut ret, msg);
428 Some(ret)
429 }
430
431 fn width(&self) -> usize {
432 cmp::min(self.max_width, self.max_print)
433 }
434}
435
436struct Throttle {
437 first: bool,
438 last_update: Instant,
439}
440
441impl Throttle {
442 fn new() -> Throttle {
443 Throttle {
444 first: true,
445 last_update: Instant::now(),
446 }
447 }
448
449 fn allowed(&mut self) -> bool {
450 if self.first {
451 let delay = Duration::from_millis(500);
452 if self.last_update.elapsed() < delay {
453 return false;
454 }
455 } else {
456 let interval = Duration::from_millis(100);
457 if self.last_update.elapsed() < interval {
458 return false;
459 }
460 }
461 self.update();
462 true
463 }
464
465 fn update(&mut self) {
466 self.first = false;
467 self.last_update = Instant::now();
468 }
469}
470
471struct TerminalIntegration {
473 enabled: bool,
474 error: bool,
475}
476
477impl TerminalIntegration {
478 #[cfg(test)]
479 fn new(enabled: bool) -> Self {
480 Self {
481 enabled,
482 error: false,
483 }
484 }
485
486 fn from_config(gctx: &GlobalContext) -> Self {
489 let enabled = gctx
490 .progress_config()
491 .term_integration
492 .unwrap_or_else(|| gctx.shell().is_err_term_integration_available());
493
494 Self {
495 enabled,
496 error: false,
497 }
498 }
499
500 fn progress_state(&self, value: StatusValue) -> StatusValue {
501 match (self.enabled, self.error) {
502 (true, false) => value,
503 (true, true) => match value {
504 StatusValue::Value(v) => StatusValue::Error(v),
505 _ => StatusValue::Error(100),
506 },
507 (false, _) => StatusValue::None,
508 }
509 }
510
511 pub fn remove(&self) -> StatusValue {
512 self.progress_state(StatusValue::Remove)
513 }
514
515 pub fn value(&self, percent: u8) -> StatusValue {
516 self.progress_state(StatusValue::Value(percent))
517 }
518
519 pub fn indeterminate(&self) -> StatusValue {
520 self.progress_state(StatusValue::Indeterminate)
521 }
522
523 pub fn error(&mut self) {
524 self.error = true;
525 }
526}
527
528enum ProgressOutput {
529 PrintNow,
531 TextAndReport(String, StatusValue),
533 Report(StatusValue),
535}
536
537#[cfg_attr(test, derive(PartialEq, Debug))]
539enum StatusValue {
540 None,
542 Remove,
544 Value(u8),
546 Indeterminate,
548 Error(u8),
550}
551
552impl std::fmt::Display for StatusValue {
553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554 let progress = match self {
555 Self::None => TermProgress::none(),
556 Self::Remove => TermProgress::remove(),
557 Self::Value(v) => TermProgress::start().percent(*v),
558 Self::Indeterminate => TermProgress::start(),
559 Self::Error(v) => TermProgress::error().percent(*v),
560 };
561
562 progress.fmt(f)
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 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 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 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}