1use std::sync::{Arc, Mutex};
2use std::time::Instant;
3
4use crate::cli::Tick;
5use crate::cli::fmt::ansi;
6use crate::cli::printer::PRINTER;
7use crate::cli::progress::{
8 BarFormatter, BarResult, ChildState, ChildStateStrong, Estimater, ProgressBarBuilder,
9};
10
11const CHAR_BAR_TICK: char = '\u{251C}'; const CHAR_BAR: char = '\u{2502}'; const CHAR_TICK: char = '\u{2514}'; #[derive(Debug)]
19pub struct ProgressBar {
20 pub(crate) state: StateImmut,
21 state_mut: Mutex<State>,
22}
23impl ProgressBar {
24 pub(crate) fn spawn(
25 state: StateImmut,
26 state_mut: State,
27 parent: Option<Arc<Self>>,
28 ) -> Arc<Self> {
29 let bar = Arc::new(Self {
30 state,
31 state_mut: Mutex::new(state_mut),
32 });
33 match parent {
34 Some(p) => {
35 if let Ok(mut p) = p.state_mut.lock() {
36 p.add_child(&bar);
37 }
38 }
39 None => {
40 if let Ok(mut printer) = PRINTER.lock() {
41 if let Some(printer) = printer.as_mut() {
42 printer.add_progress_bar(&bar);
43 }
44 }
45 }
46 }
47 bar
48 }
49 #[doc(hidden)]
50 #[inline(always)]
51 pub fn __set(self: &Arc<Self>, current: u64, message: Option<String>) {
52 if let Ok(mut bar) = self.state_mut.lock() {
53 bar.unreal_current = current;
54 if let Some(x) = message {
55 bar.set_message(&x);
56 }
57 }
58 }
59
60 #[doc(hidden)]
61 #[inline(always)]
62 pub fn __inc(self: &Arc<Self>, amount: u64, message: Option<String>) {
63 if let Ok(mut bar) = self.state_mut.lock() {
64 bar.unreal_current = bar.unreal_current.saturating_add(amount);
65 if let Some(x) = message {
66 bar.set_message(&x);
67 }
68 }
69 }
70
71 pub fn set_total(&self, total: u64) {
73 if total == 0 {
74 return;
76 }
77 if let Ok(mut bar) = self.state_mut.lock() {
78 bar.unreal_total = total;
79 }
80 }
81
82 #[inline(always)]
87 pub fn child(self: &Arc<Self>, message: impl Into<String>) -> ProgressBarBuilder {
88 ProgressBarBuilder::new(message.into()).parent(Some(Arc::clone(self)))
89 }
90
91 pub fn done_with_message(self: Arc<Self>, message: &str) {
93 self.set_done_message(message);
94 self.done();
95 }
96
97 pub fn done_by_ref_with_message(&self, message: &str) {
99 self.set_done_message(message);
100 self.done_by_ref();
101 }
102
103 pub fn set_done_message(&self, message: &str) {
109 if let Ok(mut bar) = self.state_mut.lock() {
110 bar.set_done_message(message);
111 }
112 }
113
114 pub fn set_interrupted_message(&self, message: &str) {
117 if let Ok(mut bar) = self.state_mut.lock() {
118 bar.set_interrupted_message(message);
119 }
120 }
121
122 pub fn done(self: Arc<Self>) {
130 if self.state.unbounded {
131 if let Ok(mut bar) = self.state_mut.lock() {
132 bar.unreal_current = 1;
133 bar.unreal_total = 1;
134 }
135 }
136 }
137
138 pub fn done_by_ref(&self) {
140 if self.state.unbounded {
141 if let Ok(mut bar) = self.state_mut.lock() {
142 bar.unreal_current = 1;
143 bar.unreal_total = 1;
144 }
145 }
146 }
147
148 #[inline(always)]
150 pub(crate) fn format(&self, fmt: &mut BarFormatter<'_, '_, '_>) -> i32 {
151 self.format_at_depth(0, &mut String::new(), fmt)
152 }
153
154 fn format_at_depth(
156 &self,
157 depth: usize,
158 hierarchy: &mut String,
159 fmt: &mut BarFormatter<'_, '_, '_>,
160 ) -> i32 {
161 let Ok(mut bar) = self.state_mut.lock() else {
162 return 0;
163 };
164 bar.format_at_depth(depth, hierarchy, fmt, &self.state)
165 }
166}
167
168impl Drop for ProgressBar {
169 fn drop(&mut self) {
170 let result = match self.state_mut.lock() {
171 Err(_) => BarResult::DontKeep,
172 Ok(bar) => bar.check_result(&self.state),
173 };
174 if let Some(parent) = &self.state.parent {
175 if let Ok(mut parent_state) = parent.state_mut.lock() {
177 parent_state.child_done(self.state.id, result.clone());
178 }
179 }
180 let handle = {
181 let Ok(mut printer) = PRINTER.lock() else {
183 return;
184 };
185 let Some(printer) = printer.as_mut() else {
186 return;
187 };
188 printer.print_bar_done(&result, self.state.parent.is_none());
189 printer.take_print_task_if_should_join()
190 };
191 if let Some(x) = handle {
192 let _: Result<(), _> = x.join();
193 }
194 }
195}
196
197#[derive(Debug)]
199pub struct StateImmut {
200 pub id: usize,
202 pub parent: Option<Arc<ProgressBar>>,
204 pub prefix: String,
206 pub show_percentage: bool,
208 pub unbounded: bool,
210 pub display_bytes: bool,
212 pub max_display_children: usize,
215}
216
217#[derive(Debug)]
219pub struct State {
220 unreal_total: u64,
221 unreal_current: u64,
222 message: String,
223 done_message: Option<String>,
226 interrupted_message: Option<String>,
228 eta: Option<Estimater>,
229 children: Vec<ChildState>,
230}
231impl State {
232 pub fn new(
233 total: u64,
234 eta: Option<Estimater>,
235 done_message: Option<String>,
236 interrupted_message: Option<String>,
237 ) -> Self {
238 Self {
239 unreal_total: total,
240 unreal_current: 0,
241 message: String::new(),
242 done_message,
243 interrupted_message,
244 eta,
245 children: vec![],
246 }
247 }
248 #[inline(always)]
249 fn estimate_remaining(
250 &mut self,
251 unbounded: bool,
252 now: &mut Option<Instant>,
253 tick: Tick,
254 ) -> Option<f32> {
255 if unbounded || self.unreal_total == 0 {
256 return None;
257 }
258 self.eta.as_mut()?.update(
259 now,
260 self.unreal_current.min(self.unreal_total),
261 self.unreal_total,
262 tick,
263 )
264 }
265 #[inline(always)]
266 fn real_current_total(&self, unbounded: bool) -> (u64, Option<u64>) {
267 if unbounded {
268 (0, None)
269 } else if self.unreal_total == 0 {
270 (self.unreal_current, None)
272 } else {
273 (
274 self.unreal_current.min(self.unreal_total),
275 Some(self.unreal_total),
276 )
277 }
278 }
279
280 pub fn add_child(&mut self, child: &Arc<ProgressBar>) {
281 self.children
282 .push(ChildState::Progress(child.state.id, Arc::downgrade(child)))
283 }
284
285 pub fn child_done(&mut self, child_id: usize, mut result: BarResult) {
286 self.children.retain_mut(|child| {
287 let ChildState::Progress(id, _) = child else {
288 return true;
289 };
290 if *id != child_id {
291 return true;
292 }
293 match std::mem::take(&mut result) {
294 BarResult::DontKeep => false,
295 BarResult::Done(message) => {
296 *child = ChildState::Done(message);
297 true
298 }
299 BarResult::Interrupted(message) => {
300 *child = ChildState::Interrupted(message);
301 true
302 }
303 }
304 });
305 }
306
307 pub fn check_result(&self, state: &StateImmut) -> BarResult {
308 let is_interrupted = (self.unreal_current == 0 && self.unreal_total == 0)
309 || (self.unreal_current < self.unreal_total);
310 if !is_interrupted {
311 match &self.done_message {
312 None => BarResult::DontKeep,
313 Some(message) => {
314 let message =
315 self.format_finish_message(message, state.unbounded, state.display_bytes);
316 BarResult::Done(message)
317 }
318 }
319 } else {
320 match &self.interrupted_message {
321 None => {
322 let message = if state.prefix.is_empty() {
323 self.format_finish_message(
324 "interrupted",
325 state.unbounded,
326 state.display_bytes,
327 )
328 } else {
329 self.format_finish_message(
330 &format!("{}: interrupted", state.prefix),
331 state.unbounded,
332 state.display_bytes,
333 )
334 };
335 BarResult::Interrupted(message)
336 }
337 Some(message) => {
338 let message =
339 self.format_finish_message(message, state.unbounded, state.display_bytes);
340 BarResult::Interrupted(message)
341 }
342 }
343 }
344 }
345
346 pub fn set_message(&mut self, message: &str) {
347 self.message.clear();
348 self.message.push_str(message);
349 }
350
351 pub fn set_done_message(&mut self, message: &str) {
352 match &mut self.done_message {
353 None => {
354 self.done_message = Some(message.to_string());
355 }
356 Some(msg) => {
357 msg.clear();
358 msg.push_str(message);
359 }
360 }
361 }
362
363 pub fn set_interrupted_message(&mut self, message: &str) {
364 match &mut self.interrupted_message {
365 None => {
366 self.interrupted_message = Some(message.to_string());
367 }
368 Some(msg) => {
369 msg.clear();
370 msg.push_str(message);
371 }
372 }
373 }
374
375 pub fn format_at_depth(
385 &mut self,
386 depth: usize,
387 hierarchy: &mut String,
388 fmt: &mut BarFormatter<'_, '_, '_>,
389 state: &StateImmut,
390 ) -> i32 {
391 self.format_self(fmt, fmt.width.saturating_sub((depth + 1) * 2), state);
392 fmt.out.push('\n');
393 let mut lines = 1;
394 let mut i = 0;
396 let mut num_displayed = 0;
397 let children_count = self.children.len();
398 self.children.retain_mut(|child| {
399 let out = &mut *fmt.out;
400 let Some(child) = child.upgrade() else {
401 i += 1;
402 return false; };
404 if num_displayed >= state.max_display_children {
405 num_displayed += 1;
406 return true;
407 }
408 out.push_str(". ");
410 out.push_str(fmt.colors.gray);
411 out.push_str(hierarchy);
412 if i == children_count - 1 {
413 out.push(CHAR_TICK);
414 hierarchy.push_str(" ");
415 } else {
416 out.push(CHAR_BAR_TICK);
417 hierarchy.push(CHAR_BAR);
418 hierarchy.push(' ');
419 }
420 out.push(' ');
421 let width = fmt.width.saturating_sub((depth + 2) * 2);
422 match child {
423 ChildStateStrong::Done(message) => {
424 out.push_str(fmt.colors.green);
425 format_message_with_width(out, width, message);
426 out.push('\n');
427 lines += 1;
428 out.push_str(fmt.bar_color);
429 }
430 ChildStateStrong::Interrupted(message) => {
431 out.push_str(fmt.colors.yellow);
432 format_message_with_width(out, width, message);
433 out.push('\n');
434 lines += 1;
435 out.push_str(fmt.bar_color);
436 }
437 ChildStateStrong::Progress(child) => {
438 out.push_str(fmt.bar_color);
439 lines += child.format_at_depth(depth + 1, hierarchy, fmt);
440 }
441 }
442 hierarchy.pop();
443 hierarchy.pop();
444 i += 1;
445 num_displayed += 1;
446 true
447 });
448 if num_displayed > state.max_display_children {
449 let out = &mut *fmt.out;
451 out.push_str("| ");
452 out.push_str(fmt.colors.gray);
453 for _ in 0..depth {
454 out.push(CHAR_BAR);
455 out.push(' ');
456 }
457 out.push(CHAR_TICK);
458 out.push_str(fmt.colors.reset);
459 use std::fmt::Write as _;
460 let _ = write!(
461 out,
462 " ... and {} more",
463 num_displayed - state.max_display_children
464 );
465 out.push_str(fmt.bar_color);
466 out.push('\n');
467 lines += 1;
468 }
469 lines
471 }
472
473 fn format_self(
474 &mut self,
475 fmt: &mut BarFormatter<'_, '_, '_>,
476 mut width: usize,
477 state: &StateImmut,
478 ) {
479 use std::fmt::Write as _;
480 let out = &mut *fmt.out;
481 let temp = &mut *fmt.temp;
482
483 match width {
485 0 => return,
486 1 => {
487 out.push('.');
488 return;
489 }
490 2 => {
491 out.push_str("..");
492 return;
493 }
494 3 => {
495 out.push_str("...");
496 return;
497 }
498 4 => {
499 out.push_str("[..]");
500 return;
501 }
502 _ => {}
503 }
504 let (current, total) = self.real_current_total(state.unbounded);
505 let show_current_total = !state.unbounded;
507 let show_prefix = !state.prefix.is_empty();
508 let show_percentage = state.show_percentage && total.is_some();
510 let eta = self.estimate_remaining(state.unbounded, fmt.now, fmt.tick);
511 let show_eta = eta.is_some();
512 let show_message = !self.message.is_empty();
513
514 struct Spacing {
515 show_separator: bool,
516 show_space_before_eta: bool,
517 show_space_before_message: bool,
518 }
519
520 let spacing = if state.display_bytes {
521 Spacing {
522 show_separator: show_prefix
523 && (show_current_total || show_percentage || show_eta || show_message),
524 show_space_before_eta: show_percentage || show_current_total,
525 show_space_before_message: show_percentage || show_current_total || show_eta,
526 }
527 } else {
528 Spacing {
529 show_separator: show_prefix && (show_percentage || show_eta || show_message),
530 show_space_before_eta: show_percentage,
531 show_space_before_message: show_percentage || show_eta,
532 }
533 };
534
535 if !state.display_bytes && show_current_total {
536 temp.clear();
537 let _ = match total {
539 None => write!(temp, "{current}/?"),
540 Some(total) => write!(temp, "{current}/{total}"),
541 };
542
543 width -= 2;
546 out.push('[');
547 if temp.len() > width {
548 for _ in 0..width {
550 out.push('.');
551 }
552 out.push(']');
553 return;
554 }
555
556 width -= temp.len();
557 out.push_str(temp);
558 out.push(']');
559 }
560
561 if width > 0 {
562 out.push(' ');
563 width -= 1;
564 }
565
566 if show_prefix {
567 width = format_message_with_width(out, width, &state.prefix);
568 }
569
570 if spacing.show_separator && width > 2 {
571 width -= 2;
572 out.push_str(": ");
573 }
574
575 if state.display_bytes && show_current_total {
576 temp.clear();
577 let _ = match total {
579 None => write!(temp, "{}", cu::ByteFormat(current)),
580 Some(total) => write!(
581 temp,
582 "{} / {}",
583 cu::ByteFormat(current),
584 cu::ByteFormat(total)
585 ),
586 };
587
588 if width >= temp.len() {
589 width -= temp.len();
590 out.push_str(temp);
591 }
592
593 if width > 0 {
594 out.push(' ');
595 width -= 1;
596 }
597 }
598
599 if show_percentage {
600 let total = total.unwrap();
602 if current == total {
603 if width >= 4 {
604 width -= 4;
605 out.push_str("100%")
606 }
607 } else {
608 let percentage = current as f32 * 100f32 / total as f32;
609 temp.clear();
610 let _ = write!(temp, "{percentage:.2}%");
612 if width >= temp.len() {
613 width -= temp.len();
614 out.push_str(temp);
615 }
616 }
617 }
618
619 if let Some(eta) = eta {
620 if spacing.show_space_before_eta && width > 0 {
622 out.push(' ');
623 width -= 1;
624 }
625 temp.clear();
626 let _ = write!(temp, "ETA {eta:.2}s;");
628 if width >= temp.len() {
629 width -= temp.len();
630 out.push_str(temp);
631 }
632 }
633
634 if show_message {
635 if spacing.show_space_before_message && width > 0 {
636 out.push(' ');
637 width -= 1;
638 }
639 format_message_with_width(out, width, &self.message);
640 }
641 }
642
643 fn format_finish_message(&self, message: &str, unbounded: bool, in_bytes: bool) -> String {
644 if unbounded {
645 return message.to_string();
646 }
647 let (current, total) = self.real_current_total(unbounded);
648 match (total, in_bytes) {
649 (None, false) => {
650 format!("[{current}/?] {message}")
651 }
652 (None, true) => {
653 let current = cu::ByteFormat(current);
654 format!("{message} ({current})")
655 }
656 (Some(total), false) => {
657 format!("[{current}/{total}] {message}")
658 }
659 (Some(total), true) => {
660 let current = cu::ByteFormat(current);
661 let total = cu::ByteFormat(total);
662 format!("{message} ({current} / {total})")
663 }
664 }
665 }
666}
667
668fn format_message_with_width(out: &mut String, mut width: usize, message: &str) -> usize {
669 for (c, w) in ansi::with_width(message.chars()) {
670 if w > width {
671 break;
672 }
673 width -= w;
674 out.push(c);
675 }
676 width
677}