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 term_integration: TerminalIntegration,
78 unicode: bool,
79}
80
81struct TerminalIntegration {
83 enabled: bool,
84 error: bool,
85}
86
87#[cfg_attr(test, derive(PartialEq, Debug))]
89enum StatusValue {
90 None,
92 Remove,
94 Value(f64),
96 Indeterminate,
98 Error(f64),
100}
101
102enum ProgressOutput {
103 PrintNow,
105 TextAndReport(String, StatusValue),
107 Report(StatusValue),
109}
110
111impl TerminalIntegration {
112 #[cfg(test)]
113 fn new(enabled: bool) -> Self {
114 Self {
115 enabled,
116 error: false,
117 }
118 }
119
120 fn from_config(gctx: &GlobalContext) -> Self {
123 let enabled = gctx
124 .progress_config()
125 .term_integration
126 .unwrap_or_else(|| gctx.shell().is_err_term_integration_available());
127
128 Self {
129 enabled,
130 error: false,
131 }
132 }
133
134 fn progress_state(&self, value: StatusValue) -> StatusValue {
135 match (self.enabled, self.error) {
136 (true, false) => value,
137 (true, true) => match value {
138 StatusValue::Value(v) => StatusValue::Error(v),
139 _ => StatusValue::Error(100.0),
140 },
141 (false, _) => StatusValue::None,
142 }
143 }
144
145 pub fn remove(&self) -> StatusValue {
146 self.progress_state(StatusValue::Remove)
147 }
148
149 pub fn value(&self, percent: f64) -> StatusValue {
150 self.progress_state(StatusValue::Value(percent))
151 }
152
153 pub fn indeterminate(&self) -> StatusValue {
154 self.progress_state(StatusValue::Indeterminate)
155 }
156
157 pub fn error(&mut self) {
158 self.error = true;
159 }
160}
161
162impl std::fmt::Display for StatusValue {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 let (state, progress) = match self {
172 Self::None => return Ok(()), Self::Remove => (0, 0.0),
174 Self::Value(v) => (1, *v),
175 Self::Indeterminate => (3, 0.0),
176 Self::Error(v) => (2, *v),
177 };
178 write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
179 }
180}
181
182impl<'gctx> Progress<'gctx> {
183 pub fn with_style(
194 name: &str,
195 style: ProgressStyle,
196 gctx: &'gctx GlobalContext,
197 ) -> Progress<'gctx> {
198 let dumb = match gctx.get_env("TERM") {
202 Ok(term) => term == "dumb",
203 Err(_) => false,
204 };
205 let progress_config = gctx.progress_config();
206 match progress_config.when {
207 ProgressWhen::Always => return Progress::new_priv(name, style, gctx),
208 ProgressWhen::Never => return Progress { state: None },
209 ProgressWhen::Auto => {}
210 }
211 if gctx.shell().verbosity() == Verbosity::Quiet || dumb || is_ci() {
212 return Progress { state: None };
213 }
214 Progress::new_priv(name, style, gctx)
215 }
216
217 fn new_priv(name: &str, style: ProgressStyle, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
218 let progress_config = gctx.progress_config();
219 let width = progress_config
220 .width
221 .or_else(|| gctx.shell().err_width().progress_max_width());
222
223 Progress {
224 state: width.map(|n| State {
225 gctx,
226 format: Format {
227 style,
228 max_width: n,
229 max_print: 50,
232 term_integration: TerminalIntegration::from_config(gctx),
233 unicode: gctx.shell().err_unicode(),
234 },
235 name: name.to_string(),
236 done: false,
237 throttle: Throttle::new(),
238 last_line: None,
239 fixed_width: progress_config.width,
240 }),
241 }
242 }
243
244 pub fn disable(&mut self) {
246 self.state = None;
247 }
248
249 pub fn is_enabled(&self) -> bool {
251 self.state.is_some()
252 }
253
254 pub fn new(name: &str, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
258 Self::with_style(name, ProgressStyle::Percentage, gctx)
259 }
260
261 pub fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
271 let Some(s) = &mut self.state else {
272 return Ok(());
273 };
274
275 if !s.throttle.allowed() {
288 return Ok(());
289 }
290
291 s.tick(cur, max, msg)
292 }
293
294 pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
303 match self.state {
304 Some(ref mut s) => s.tick(cur, max, msg),
305 None => Ok(()),
306 }
307 }
308
309 pub fn update_allowed(&mut self) -> bool {
314 match &mut self.state {
315 Some(s) => s.throttle.allowed(),
316 None => false,
317 }
318 }
319
320 pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
329 match &mut self.state {
330 Some(s) => s.print(ProgressOutput::PrintNow, msg),
331 None => Ok(()),
332 }
333 }
334
335 pub fn clear(&mut self) {
337 if let Some(ref mut s) = self.state {
338 s.clear();
339 }
340 }
341
342 pub fn indicate_error(&mut self) {
344 if let Some(s) = &mut self.state {
345 s.format.term_integration.error()
346 }
347 }
348}
349
350impl Throttle {
351 fn new() -> Throttle {
352 Throttle {
353 first: true,
354 last_update: Instant::now(),
355 }
356 }
357
358 fn allowed(&mut self) -> bool {
359 if self.first {
360 let delay = Duration::from_millis(500);
361 if self.last_update.elapsed() < delay {
362 return false;
363 }
364 } else {
365 let interval = Duration::from_millis(100);
366 if self.last_update.elapsed() < interval {
367 return false;
368 }
369 }
370 self.update();
371 true
372 }
373
374 fn update(&mut self) {
375 self.first = false;
376 self.last_update = Instant::now();
377 }
378}
379
380impl<'gctx> State<'gctx> {
381 fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
382 if self.done {
383 write!(
384 self.gctx.shell().err(),
385 "{}",
386 self.format.term_integration.remove()
387 )?;
388 return Ok(());
389 }
390
391 if max > 0 && cur == max {
392 self.done = true;
393 }
394
395 self.try_update_max_width();
398 if let Some(pbar) = self.format.progress(cur, max) {
399 self.print(pbar, msg)?;
400 }
401 Ok(())
402 }
403
404 fn print(&mut self, progress: ProgressOutput, msg: &str) -> CargoResult<()> {
405 self.throttle.update();
406 self.try_update_max_width();
407
408 let (mut line, report) = match progress {
409 ProgressOutput::PrintNow => (String::new(), None),
410 ProgressOutput::TextAndReport(prefix, report) => (prefix, Some(report)),
411 ProgressOutput::Report(report) => (String::new(), Some(report)),
412 };
413
414 if self.format.max_width < 15 {
416 if let Some(tb) = report {
418 write!(self.gctx.shell().err(), "{tb}\r")?;
419 }
420 return Ok(());
421 }
422
423 self.format.render(&mut line, msg);
424 while line.len() < self.format.max_width - 15 {
425 line.push(' ');
426 }
427
428 if self.gctx.shell().is_cleared() || self.last_line.as_ref() != Some(&line) {
430 let mut shell = self.gctx.shell();
431 shell.set_needs_clear(false);
432 shell.status_header(&self.name)?;
433 if let Some(tb) = report {
434 write!(shell.err(), "{line}{tb}\r")?;
435 } else {
436 write!(shell.err(), "{line}\r")?;
437 }
438 self.last_line = Some(line);
439 shell.set_needs_clear(true);
440 }
441
442 Ok(())
443 }
444
445 fn clear(&mut self) {
446 let _ = write!(
448 self.gctx.shell().err(),
449 "{}",
450 self.format.term_integration.remove()
451 );
452 if self.last_line.is_some() && !self.gctx.shell().is_cleared() {
454 self.gctx.shell().err_erase_line();
455 self.last_line = None;
456 }
457 }
458
459 fn try_update_max_width(&mut self) {
460 if self.fixed_width.is_none() {
461 if let Some(n) = self.gctx.shell().err_width().progress_max_width() {
462 self.format.max_width = n;
463 }
464 }
465 }
466}
467
468impl Format {
469 fn progress(&self, cur: usize, max: usize) -> Option<ProgressOutput> {
470 assert!(cur <= max);
471 let pct = (cur as f64) / (max as f64);
474 let pct = if !pct.is_finite() { 0.0 } else { pct };
475 let stats = match self.style {
476 ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
477 ProgressStyle::Ratio => format!(" {cur}/{max}"),
478 ProgressStyle::Indeterminate => String::new(),
479 };
480 let report = match self.style {
481 ProgressStyle::Percentage | ProgressStyle::Ratio => {
482 self.term_integration.value(pct * 100.0)
483 }
484 ProgressStyle::Indeterminate => self.term_integration.indeterminate(),
485 };
486
487 let extra_len = stats.len() + 2 + 15 ;
488 let Some(display_width) = self.width().checked_sub(extra_len) else {
489 if self.term_integration.enabled {
490 return Some(ProgressOutput::Report(report));
491 }
492 return None;
493 };
494
495 let mut string = String::with_capacity(self.max_width);
496 string.push('[');
497 let hashes = display_width as f64 * pct;
498 let hashes = hashes as usize;
499
500 if hashes > 0 {
502 for _ in 0..hashes - 1 {
503 string.push('=');
504 }
505 if cur == max {
506 string.push('=');
507 } else {
508 string.push('>');
509 }
510 }
511
512 for _ in 0..(display_width - hashes) {
514 string.push(' ');
515 }
516 string.push(']');
517 string.push_str(&stats);
518
519 Some(ProgressOutput::TextAndReport(string, report))
520 }
521
522 fn render(&self, string: &mut String, msg: &str) {
523 let mut avail_msg_len = self.max_width - string.len() - 15;
524 let mut ellipsis_pos = 0;
525
526 let (ellipsis, ellipsis_width) = if self.unicode { ("…", 1) } else { ("...", 3) };
527
528 if avail_msg_len <= ellipsis_width {
529 return;
530 }
531 for c in msg.chars() {
532 let display_width = c.width().unwrap_or(0);
533 if avail_msg_len >= display_width {
534 avail_msg_len -= display_width;
535 string.push(c);
536 if avail_msg_len >= ellipsis_width {
537 ellipsis_pos = string.len();
538 }
539 } else {
540 string.truncate(ellipsis_pos);
541 string.push_str(ellipsis);
542 break;
543 }
544 }
545 }
546
547 #[cfg(test)]
548 fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
549 let mut ret = match self.progress(cur, max)? {
550 ProgressOutput::TextAndReport(text, _) => text,
552 _ => return None,
553 };
554 self.render(&mut ret, msg);
555 Some(ret)
556 }
557
558 fn width(&self) -> usize {
559 cmp::min(self.max_width, self.max_print)
560 }
561}
562
563impl<'gctx> Drop for State<'gctx> {
564 fn drop(&mut self) {
565 self.clear();
566 }
567}
568
569#[test]
570fn test_progress_status() {
571 let format = Format {
572 style: ProgressStyle::Ratio,
573 max_print: 40,
574 max_width: 60,
575 term_integration: TerminalIntegration::new(false),
576 unicode: true,
577 };
578 assert_eq!(
579 format.progress_status(0, 4, ""),
580 Some("[ ] 0/4".to_string())
581 );
582 assert_eq!(
583 format.progress_status(1, 4, ""),
584 Some("[===> ] 1/4".to_string())
585 );
586 assert_eq!(
587 format.progress_status(2, 4, ""),
588 Some("[========> ] 2/4".to_string())
589 );
590 assert_eq!(
591 format.progress_status(3, 4, ""),
592 Some("[=============> ] 3/4".to_string())
593 );
594 assert_eq!(
595 format.progress_status(4, 4, ""),
596 Some("[===================] 4/4".to_string())
597 );
598
599 assert_eq!(
600 format.progress_status(3999, 4000, ""),
601 Some("[===========> ] 3999/4000".to_string())
602 );
603 assert_eq!(
604 format.progress_status(4000, 4000, ""),
605 Some("[=============] 4000/4000".to_string())
606 );
607
608 assert_eq!(
609 format.progress_status(3, 4, ": short message"),
610 Some("[=============> ] 3/4: short message".to_string())
611 );
612 assert_eq!(
613 format.progress_status(3, 4, ": msg thats just fit"),
614 Some("[=============> ] 3/4: msg thats just fit".to_string())
615 );
616 assert_eq!(
617 format.progress_status(3, 4, ": msg that's just fit"),
618 Some("[=============> ] 3/4: msg that's just f…".to_string())
619 );
620
621 let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
623 assert_eq!(
624 format.progress_status(3, 4, zalgo_msg),
625 Some("[=============> ] 3/4".to_string() + zalgo_msg)
626 );
627
628 assert_eq!(
630 format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
631 Some("[=============> ] 3/4_123456789123456e\u{301}\u{301}8\u{301}9…".to_string())
632 );
633 assert_eq!(
634 format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
635 Some("[=============> ] 3/4:每個漢字佔據了兩…".to_string())
636 );
637 assert_eq!(
638 format.progress_status(3, 4, ":-每個漢字佔據了兩個字元"),
640 Some("[=============> ] 3/4:-每個漢字佔據了兩…".to_string())
641 );
642}
643
644#[test]
645fn test_progress_status_percentage() {
646 let format = Format {
647 style: ProgressStyle::Percentage,
648 max_print: 40,
649 max_width: 60,
650 term_integration: TerminalIntegration::new(false),
651 unicode: true,
652 };
653 assert_eq!(
654 format.progress_status(0, 77, ""),
655 Some("[ ] 0.00%".to_string())
656 );
657 assert_eq!(
658 format.progress_status(1, 77, ""),
659 Some("[ ] 1.30%".to_string())
660 );
661 assert_eq!(
662 format.progress_status(76, 77, ""),
663 Some("[=============> ] 98.70%".to_string())
664 );
665 assert_eq!(
666 format.progress_status(77, 77, ""),
667 Some("[===============] 100.00%".to_string())
668 );
669}
670
671#[test]
672fn test_progress_status_too_short() {
673 let format = Format {
674 style: ProgressStyle::Percentage,
675 max_print: 25,
676 max_width: 25,
677 term_integration: TerminalIntegration::new(false),
678 unicode: true,
679 };
680 assert_eq!(
681 format.progress_status(1, 1, ""),
682 Some("[] 100.00%".to_string())
683 );
684
685 let format = Format {
686 style: ProgressStyle::Percentage,
687 max_print: 24,
688 max_width: 24,
689 term_integration: TerminalIntegration::new(false),
690 unicode: true,
691 };
692 assert_eq!(format.progress_status(1, 1, ""), None);
693}
694
695#[test]
696fn test_term_integration_disabled() {
697 let report = TerminalIntegration::new(false);
698 let mut out = String::new();
699 out.push_str(&report.remove().to_string());
700 out.push_str(&report.value(10.0).to_string());
701 out.push_str(&report.indeterminate().to_string());
702 assert!(out.is_empty());
703}
704
705#[test]
706fn test_term_integration_error_state() {
707 let mut report = TerminalIntegration::new(true);
708 assert_eq!(report.value(10.0), StatusValue::Value(10.0));
709 report.error();
710 assert_eq!(report.value(50.0), StatusValue::Error(50.0));
711}