Skip to main content

pistonite_cu/cli/progress/
state.rs

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}'; // |>
12const CHAR_BAR: char = '\u{2502}'; // |
13const CHAR_TICK: char = '\u{2514}'; // >
14
15/// Handle for a progress bar (This is the internal state, the handle is `Arc<ProgressBar>`)
16///
17/// See [Progress Bars](fn@crate::progress)
18#[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    /// Set the total steps (if the progress is finite)
72    pub fn set_total(&self, total: u64) {
73        if total == 0 {
74            // 0 is a special value, so we do not allow setting it
75            return;
76        }
77        if let Ok(mut bar) = self.state_mut.lock() {
78            bar.unreal_total = total;
79        }
80    }
81
82    /// Start building a child progress bar
83    ///
84    /// Note that the child builder will keep this bar alive (displayed), even
85    /// if the child is not spawned
86    #[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    /// Set the message to be printed and mark the bar as done
92    pub fn done_with_message(self: Arc<Self>, message: &str) {
93        self.set_done_message(message);
94        self.done();
95    }
96
97    /// Set the message to be printed and mark the bar as done
98    pub fn done_by_ref_with_message(&self, message: &str) {
99        self.set_done_message(message);
100        self.done_by_ref();
101    }
102
103    /// Change the message to be printed when the bar is done.
104    /// Useful if information is not available yet when the bar is spawned.
105    ///
106    /// This does NOT mark the bar as done, use [`done_with_message`](Self::done_with_message)
107    /// for that.
108    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    /// Change the message to be printed when the bar is interrupted.
115    /// Useful if information is not available yet when the bar is spawned.
116    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    /// Mark the progress bar as done and drop the handle.
123    ///
124    /// This needs to be called if the bar is unbounded. Otherwise,
125    /// the bar will display in the interrupted state when dropped.
126    ///
127    /// If the progress is finite, then interrupted state is automatically
128    /// determined (`current != total`)
129    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    /// Same as [`done`](Self::done), but does not drop the bar.
139    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    /// Format the bar
149    #[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    /// Format the bar at depth
155    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            // inform parent our result
176            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            // scope for printer lock
182            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/// Internal, immutable state of progress bar
198#[derive(Debug)]
199pub struct StateImmut {
200    /// An ID
201    pub id: usize,
202    /// Parent of this bar
203    pub parent: Option<Arc<ProgressBar>>,
204    /// The prefix message (corresponds to message in the builder)
205    pub prefix: String,
206    /// If percentage field is shown
207    pub show_percentage: bool,
208    /// If the steps are unbounded
209    pub unbounded: bool,
210    /// Display the progress using bytes format
211    pub display_bytes: bool,
212    /// Max number of children to display,
213    /// children after the limit will only display one line "... and X more"
214    pub max_display_children: usize,
215}
216
217/// Internal mutable state
218#[derive(Debug)]
219pub struct State {
220    unreal_total: u64,
221    unreal_current: u64,
222    message: String,
223    /// None means don't keep the progress bar printed
224    /// (the default done message is formatted at spawn time)
225    done_message: Option<String>,
226    /// None means use the default
227    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            // total not known
271            (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    /// Format the bar into the out buffer at the depth
376    ///
377    /// If depth is 0, the animation character is already formatted.
378    /// Otherwise, a "| " should be formatted into the out buffer
379    /// at the beginning. The `width` passed in is terminal width minus 2.
380    ///
381    /// It should also format a new line character into the buffer
382    ///
383    /// Return number of lines formatted.
384    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        // process childrens
395        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; // remove the finished child
403            };
404            if num_displayed >= state.max_display_children {
405                num_displayed += 1;
406                return true;
407            }
408            // format the multi-line syntax
409            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            // display the ... and more line
450            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        // return number of lines
470        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        // not enough width
484        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        // --
506        let show_current_total = !state.unbounded;
507        let show_prefix = !state.prefix.is_empty();
508        // -- :
509        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            // _: fmt for string does not fail
538            let _ = match total {
539                None => write!(temp, "{current}/?"),
540                Some(total) => write!(temp, "{current}/{total}"),
541            };
542
543            // .len() is safe because / and numbers have the same byte size and width
544            // -2 is safe because width > 4 here
545            width -= 2;
546            out.push('[');
547            if temp.len() > width {
548                // not enough space
549                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            // _: fmt for string does not fail
578            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            // unwrap: total is always Some
601            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                // _: fmt for string does not fail
611                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            // ETA SS.SSs
621            if spacing.show_space_before_eta && width > 0 {
622                out.push(' ');
623                width -= 1;
624            }
625            temp.clear();
626            // _: fmt for string does not fail
627            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}