1use std::cmp;
4use std::time::{Duration, Instant};
5
6use crate::context::ProgressWhen;
7use crate::util::{CargoResult, GlobalContext};
8use anstyle_progress::TermProgress;
9use cargo_util_terminal::Shell;
10use unicode_width::UnicodeWidthChar;
11
12pub struct Progress<'gctx> {
30 gctx: &'gctx GlobalContext,
31 state: Option<State>,
32}
33
34impl<'gctx> Progress<'gctx> {
35 pub fn new(name: &str, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
39 Self::with_style(name, ProgressStyle::Percentage, gctx)
40 }
41
42 pub fn with_style(
53 name: &str,
54 style: ProgressStyle,
55 gctx: &'gctx GlobalContext,
56 ) -> Progress<'gctx> {
57 let progress_config = gctx.progress_config();
58 let progress = match progress_config.when {
59 ProgressWhen::Always => true,
60 ProgressWhen::Never => false,
61 ProgressWhen::Auto => gctx.shell().progress_supported(),
62 };
63 if progress {
64 Progress::new_priv(name, style, gctx)
65 } else {
66 return Progress { gctx, state: None };
67 }
68 }
69
70 fn new_priv(name: &str, style: ProgressStyle, gctx: &'gctx GlobalContext) -> Progress<'gctx> {
71 let progress_config = gctx.progress_config();
72 let width = progress_config
73 .width
74 .or_else(|| gctx.shell().err_width().progress_max_width());
75
76 Progress {
77 gctx,
78 state: width.map(|n| State {
79 format: Format {
80 style,
81 max_width: n,
82 max_print: 50,
85 term_integration: TerminalIntegration::from_config(gctx),
86 unicode: gctx.shell().err_unicode(),
87 },
88 name: name.to_string(),
89 done: false,
90 throttle: Throttle::new(),
91 last_line: None,
92 fixed_width: progress_config.width,
93 }),
94 }
95 }
96
97 pub fn disable(&mut self) {
99 self.state = None;
100 }
101
102 pub fn is_enabled(&self) -> bool {
104 self.state.is_some()
105 }
106
107 pub fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
117 let Some(s) = &mut self.state else {
118 return Ok(());
119 };
120
121 let mut shell = self.gctx.shell();
122
123 if !s.throttle.allowed() {
136 return Ok(());
137 }
138
139 s.tick(cur, max, msg, &mut shell)
140 }
141
142 pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
151 let mut shell = self.gctx.shell();
152
153 match self.state {
154 Some(ref mut s) => s.tick(cur, max, msg, &mut shell),
155 None => Ok(()),
156 }
157 }
158
159 pub fn update_allowed(&mut self) -> bool {
164 match &mut self.state {
165 Some(s) => s.throttle.allowed(),
166 None => false,
167 }
168 }
169
170 pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
179 let mut shell = self.gctx.shell();
180
181 match &mut self.state {
182 Some(s) => s.print(ProgressOutput::PrintNow, msg, &mut shell),
183 None => Ok(()),
184 }
185 }
186
187 pub fn clear(&mut self) {
189 let mut shell = self.gctx.shell();
190
191 if let Some(ref mut s) = self.state {
192 s.clear(&mut shell);
193 }
194 }
195
196 pub fn indicate_error(&mut self) {
198 if let Some(s) = &mut self.state {
199 s.format.term_integration.error()
200 }
201 }
202}
203
204impl<'gctx> Drop for Progress<'gctx> {
205 fn drop(&mut self) {
206 self.clear();
207 }
208}
209
210pub enum ProgressStyle {
214 Percentage,
220 Ratio,
226 Indeterminate,
233}
234
235struct State {
236 format: Format,
237 name: String,
238 done: bool,
239 throttle: Throttle,
240 last_line: Option<String>,
241 fixed_width: Option<usize>,
242}
243
244impl State {
245 fn tick(&mut self, cur: usize, max: usize, msg: &str, shell: &mut Shell) -> CargoResult<()> {
246 if self.done {
247 write!(shell.err(), "{}", self.format.term_integration.remove())?;
248 return Ok(());
249 }
250
251 if max > 0 && cur == max {
252 self.done = true;
253 }
254
255 self.try_update_max_width(shell);
258 if let Some(pbar) = self.format.progress(cur, max) {
259 self.print(pbar, msg, shell)?;
260 }
261 Ok(())
262 }
263
264 fn print(&mut self, progress: ProgressOutput, msg: &str, shell: &mut Shell) -> CargoResult<()> {
265 self.throttle.update();
266 self.try_update_max_width(shell);
267
268 let (mut line, report) = match progress {
269 ProgressOutput::PrintNow => (String::new(), None),
270 ProgressOutput::TextAndReport(prefix, report) => (prefix, Some(report)),
271 ProgressOutput::Report(report) => (String::new(), Some(report)),
272 };
273
274 if self.format.max_width < 15 {
276 if let Some(tb) = report {
278 write!(shell.err(), "{tb}\r")?;
279 }
280 return Ok(());
281 }
282
283 self.format.render(&mut line, msg);
284 while line.len() < self.format.max_width - 15 {
285 line.push(' ');
286 }
287
288 if shell.is_cleared() || self.last_line.as_ref() != Some(&line) {
290 shell.set_needs_clear(false);
291 shell.transient_status(&self.name)?;
292 if let Some(tb) = report {
293 write!(shell.err(), "{line}{tb}\r")?;
294 } else {
295 write!(shell.err(), "{line}\r")?;
296 }
297 self.last_line = Some(line);
298 shell.set_needs_clear(true);
299 }
300
301 Ok(())
302 }
303
304 fn clear(&mut self, shell: &mut Shell) {
305 let _ = write!(shell.err(), "{}", self.format.term_integration.remove());
307 if self.last_line.is_some() && !shell.is_cleared() {
309 shell.err_erase_line();
310 self.last_line = None;
311 }
312 }
313
314 fn try_update_max_width(&mut self, shell: &mut Shell) {
315 if self.fixed_width.is_none() {
316 if let Some(n) = shell.err_width().progress_max_width() {
317 self.format.max_width = n;
318 }
319 }
320 }
321}
322
323struct Format {
324 style: ProgressStyle,
325 max_width: usize,
326 max_print: usize,
327 term_integration: TerminalIntegration,
328 unicode: bool,
329}
330
331impl Format {
332 fn progress(&self, cur: usize, max: usize) -> Option<ProgressOutput> {
333 assert!(cur <= max);
334 let pct = (cur as f64) / (max as f64);
337 let pct = if !pct.is_finite() { 0.0 } else { pct };
338 let stats = match self.style {
339 ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
340 ProgressStyle::Ratio => format!(" {cur}/{max}"),
341 ProgressStyle::Indeterminate => String::new(),
342 };
343 let report = match self.style {
344 ProgressStyle::Percentage | ProgressStyle::Ratio => {
345 let pct = (pct * 100.0) as u8;
346 let pct = pct.clamp(0, 100);
347 self.term_integration.value(pct)
348 }
349 ProgressStyle::Indeterminate => self.term_integration.indeterminate(),
350 };
351
352 let extra_len = stats.len() + 2 + 15 ;
353 let Some(display_width) = self.width().checked_sub(extra_len) else {
354 if self.term_integration.enabled {
355 return Some(ProgressOutput::Report(report));
356 }
357 return None;
358 };
359
360 let mut string = String::with_capacity(self.max_width);
361 string.push('[');
362 let hashes = display_width as f64 * pct;
363 let hashes = hashes as usize;
364
365 if hashes > 0 {
367 for _ in 0..hashes - 1 {
368 string.push('=');
369 }
370 if cur == max {
371 string.push('=');
372 } else {
373 string.push('>');
374 }
375 }
376
377 for _ in 0..(display_width - hashes) {
379 string.push(' ');
380 }
381 string.push(']');
382 string.push_str(&stats);
383
384 Some(ProgressOutput::TextAndReport(string, report))
385 }
386
387 fn render(&self, string: &mut String, msg: &str) {
388 let mut avail_msg_len = self.max_width - string.len() - 15;
389 let mut ellipsis_pos = 0;
390
391 let (ellipsis, ellipsis_width) = if self.unicode { ("…", 1) } else { ("...", 3) };
392
393 if avail_msg_len <= ellipsis_width {
394 return;
395 }
396 for c in msg.chars() {
397 let display_width = c.width().unwrap_or(0);
398 if avail_msg_len >= display_width {
399 avail_msg_len -= display_width;
400 string.push(c);
401 if avail_msg_len >= ellipsis_width {
402 ellipsis_pos = string.len();
403 }
404 } else {
405 string.truncate(ellipsis_pos);
406 string.push_str(ellipsis);
407 break;
408 }
409 }
410 }
411
412 #[cfg(test)]
413 fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
414 let mut ret = match self.progress(cur, max)? {
415 ProgressOutput::TextAndReport(text, _) => text,
417 _ => return None,
418 };
419 self.render(&mut ret, msg);
420 Some(ret)
421 }
422
423 fn width(&self) -> usize {
424 cmp::min(self.max_width, self.max_print)
425 }
426}
427
428struct Throttle {
429 first: bool,
430 last_update: Instant,
431}
432
433impl Throttle {
434 fn new() -> Throttle {
435 Throttle {
436 first: true,
437 last_update: Instant::now(),
438 }
439 }
440
441 fn allowed(&mut self) -> bool {
442 if self.first {
443 let delay = Duration::from_millis(500);
444 if self.last_update.elapsed() < delay {
445 return false;
446 }
447 } else {
448 let interval = Duration::from_millis(100);
449 if self.last_update.elapsed() < interval {
450 return false;
451 }
452 }
453 self.update();
454 true
455 }
456
457 fn update(&mut self) {
458 self.first = false;
459 self.last_update = Instant::now();
460 }
461}
462
463struct TerminalIntegration {
465 enabled: bool,
466 error: bool,
467}
468
469impl TerminalIntegration {
470 #[cfg(test)]
471 fn new(enabled: bool) -> Self {
472 Self {
473 enabled,
474 error: false,
475 }
476 }
477
478 fn from_config(gctx: &GlobalContext) -> Self {
481 let enabled = gctx
482 .progress_config()
483 .term_integration
484 .unwrap_or_else(|| gctx.shell().is_err_term_integration_available());
485
486 Self {
487 enabled,
488 error: false,
489 }
490 }
491
492 fn progress_state(&self, value: StatusValue) -> StatusValue {
493 match (self.enabled, self.error) {
494 (true, false) => value,
495 (true, true) => match value {
496 StatusValue::Value(v) => StatusValue::Error(v),
497 _ => StatusValue::Error(100),
498 },
499 (false, _) => StatusValue::None,
500 }
501 }
502
503 pub fn remove(&self) -> StatusValue {
504 self.progress_state(StatusValue::Remove)
505 }
506
507 pub fn value(&self, percent: u8) -> StatusValue {
508 self.progress_state(StatusValue::Value(percent))
509 }
510
511 pub fn indeterminate(&self) -> StatusValue {
512 self.progress_state(StatusValue::Indeterminate)
513 }
514
515 pub fn error(&mut self) {
516 self.error = true;
517 }
518}
519
520enum ProgressOutput {
521 PrintNow,
523 TextAndReport(String, StatusValue),
525 Report(StatusValue),
527}
528
529#[cfg_attr(test, derive(PartialEq, Debug))]
531enum StatusValue {
532 None,
534 Remove,
536 Value(u8),
538 Indeterminate,
540 Error(u8),
542}
543
544impl std::fmt::Display for StatusValue {
545 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546 let progress = match self {
547 Self::None => TermProgress::none(),
548 Self::Remove => TermProgress::remove(),
549 Self::Value(v) => TermProgress::start().percent(*v),
550 Self::Indeterminate => TermProgress::start(),
551 Self::Error(v) => TermProgress::error().percent(*v),
552 };
553
554 progress.fmt(f)
555 }
556}
557
558#[test]
559fn test_progress_status() {
560 let format = Format {
561 style: ProgressStyle::Ratio,
562 max_print: 40,
563 max_width: 60,
564 term_integration: TerminalIntegration::new(false),
565 unicode: true,
566 };
567 assert_eq!(
568 format.progress_status(0, 4, ""),
569 Some("[ ] 0/4".to_string())
570 );
571 assert_eq!(
572 format.progress_status(1, 4, ""),
573 Some("[===> ] 1/4".to_string())
574 );
575 assert_eq!(
576 format.progress_status(2, 4, ""),
577 Some("[========> ] 2/4".to_string())
578 );
579 assert_eq!(
580 format.progress_status(3, 4, ""),
581 Some("[=============> ] 3/4".to_string())
582 );
583 assert_eq!(
584 format.progress_status(4, 4, ""),
585 Some("[===================] 4/4".to_string())
586 );
587
588 assert_eq!(
589 format.progress_status(3999, 4000, ""),
590 Some("[===========> ] 3999/4000".to_string())
591 );
592 assert_eq!(
593 format.progress_status(4000, 4000, ""),
594 Some("[=============] 4000/4000".to_string())
595 );
596
597 assert_eq!(
598 format.progress_status(3, 4, ": short message"),
599 Some("[=============> ] 3/4: short message".to_string())
600 );
601 assert_eq!(
602 format.progress_status(3, 4, ": msg thats just fit"),
603 Some("[=============> ] 3/4: msg thats just fit".to_string())
604 );
605 assert_eq!(
606 format.progress_status(3, 4, ": msg that's just fit"),
607 Some("[=============> ] 3/4: msg that's just f…".to_string())
608 );
609
610 let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
612 assert_eq!(
613 format.progress_status(3, 4, zalgo_msg),
614 Some("[=============> ] 3/4".to_string() + zalgo_msg)
615 );
616
617 assert_eq!(
619 format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
620 Some("[=============> ] 3/4_123456789123456e\u{301}\u{301}8\u{301}9…".to_string())
621 );
622 assert_eq!(
623 format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
624 Some("[=============> ] 3/4:每個漢字佔據了兩…".to_string())
625 );
626 assert_eq!(
627 format.progress_status(3, 4, ":-每個漢字佔據了兩個字元"),
629 Some("[=============> ] 3/4:-每個漢字佔據了兩…".to_string())
630 );
631}
632
633#[test]
634fn test_progress_status_percentage() {
635 let format = Format {
636 style: ProgressStyle::Percentage,
637 max_print: 40,
638 max_width: 60,
639 term_integration: TerminalIntegration::new(false),
640 unicode: true,
641 };
642 assert_eq!(
643 format.progress_status(0, 77, ""),
644 Some("[ ] 0.00%".to_string())
645 );
646 assert_eq!(
647 format.progress_status(1, 77, ""),
648 Some("[ ] 1.30%".to_string())
649 );
650 assert_eq!(
651 format.progress_status(76, 77, ""),
652 Some("[=============> ] 98.70%".to_string())
653 );
654 assert_eq!(
655 format.progress_status(77, 77, ""),
656 Some("[===============] 100.00%".to_string())
657 );
658}
659
660#[test]
661fn test_progress_status_too_short() {
662 let format = Format {
663 style: ProgressStyle::Percentage,
664 max_print: 25,
665 max_width: 25,
666 term_integration: TerminalIntegration::new(false),
667 unicode: true,
668 };
669 assert_eq!(
670 format.progress_status(1, 1, ""),
671 Some("[] 100.00%".to_string())
672 );
673
674 let format = Format {
675 style: ProgressStyle::Percentage,
676 max_print: 24,
677 max_width: 24,
678 term_integration: TerminalIntegration::new(false),
679 unicode: true,
680 };
681 assert_eq!(format.progress_status(1, 1, ""), None);
682}
683
684#[test]
685fn test_term_integration_disabled() {
686 let report = TerminalIntegration::new(false);
687 let mut out = String::new();
688 out.push_str(&report.remove().to_string());
689 out.push_str(&report.value(10).to_string());
690 out.push_str(&report.indeterminate().to_string());
691 assert!(out.is_empty());
692}
693
694#[test]
695fn test_term_integration_error_state() {
696 let mut report = TerminalIntegration::new(true);
697 assert_eq!(report.value(10), StatusValue::Value(10));
698 report.error();
699 assert_eq!(report.value(50), StatusValue::Error(50));
700}