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