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 anstyle_progress::TermProgress;
10use cargo_util::is_ci;
11use unicode_width::UnicodeWidthChar;
12
13pub struct Progress<'gctx> {
31 state: Option<State<'gctx>>,
32}
33
34pub enum ProgressStyle {
38 Percentage,
44 Ratio,
50 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
82struct TerminalIntegration {
84 enabled: bool,
85 error: bool,
86}
87
88#[cfg_attr(test, derive(PartialEq, Debug))]
90enum StatusValue {
91 None,
93 Remove,
95 Value(u8),
97 Indeterminate,
99 Error(u8),
101}
102
103enum ProgressOutput {
104 PrintNow,
106 TextAndReport(String, StatusValue),
108 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 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 pub fn with_style(
189 name: &str,
190 style: ProgressStyle,
191 gctx: &'gctx GlobalContext,
192 ) -> Progress<'gctx> {
193 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 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 pub fn disable(&mut self) {
241 self.state = None;
242 }
243
244 pub fn is_enabled(&self) -> bool {
246 self.state.is_some()
247 }
248
249 pub fn new(name: &str, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
253 Self::with_style(name, ProgressStyle::Percentage, gctx)
254 }
255
256 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 if !s.throttle.allowed() {
283 return Ok(());
284 }
285
286 s.tick(cur, max, msg)
287 }
288
289 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 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 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 pub fn clear(&mut self) {
332 if let Some(ref mut s) = self.state {
333 s.clear();
334 }
335 }
336
337 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 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 if self.format.max_width < 15 {
411 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 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 let _ = write!(
443 self.gctx.shell().err(),
444 "{}",
445 self.format.term_integration.remove()
446 );
447 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 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 + 15 ;
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 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 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 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 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}