Skip to main content

cliclack/
multiprogress.rs

1use std::{
2    fmt::Display,
3    sync::{
4        atomic::{AtomicUsize, Ordering},
5        Arc, RwLock,
6    },
7};
8
9use console::Term;
10
11use crate::{progress::ProgressBar, theme::THEME, ThemeState};
12
13const HEADER_HEIGHT: usize = 1;
14
15/// Renders other progress bars and spinners under a common header in a single visual block.
16#[derive(Clone)]
17pub struct MultiProgress {
18    multi: indicatif::MultiProgress,
19    bars: Arc<RwLock<Vec<ProgressBar>>>,
20    prompt: String,
21    logs: Arc<AtomicUsize>,
22}
23
24impl MultiProgress {
25    /// Creates a new multi-progress bar with a given prompt.
26    pub fn new(prompt: impl Display) -> Self {
27        let theme = THEME.read().unwrap();
28        let multi = indicatif::MultiProgress::new();
29
30        let header =
31            theme.format_header(&ThemeState::Active, (prompt.to_string() + "\n ").trim_end());
32
33        multi.println(header).ok();
34
35        Self {
36            multi,
37            bars: Default::default(),
38            prompt: prompt.to_string(),
39            logs: Default::default(),
40        }
41    }
42
43    /// Adds a progress bar and returns an internalized reference to it.
44    ///
45    /// The progress bar will be positioned below all other bars in the [`MultiProgress`].
46    pub fn add(&self, pb: ProgressBar) -> ProgressBar {
47        let bars_count = self.length();
48        self.insert(bars_count, pb)
49    }
50
51    /// Inserts a progress bar at a given index and returns an internalized reference to it.
52    ///
53    /// If the index is greater than or equal to the number of progress bars, the bar is added to the end.
54    pub fn insert(&self, index: usize, pb: ProgressBar) -> ProgressBar {
55        let bars_count = self.length();
56        let index = index.min(bars_count);
57        if index == bars_count {
58            // Unset the last flag for all other progress bars: it affects rendering.
59            for bar in self.bars.write().unwrap().iter_mut() {
60                bar.options_write().last = false;
61                bar.redraw_active();
62            }
63        }
64        // Attention: deconstructing `pb` to avoid borrowing `pb.bar` twice.
65        let ProgressBar { bar, options } = pb;
66        let bar = self.multi.insert(index, bar);
67        {
68            let mut options = options.write().unwrap();
69            options.grouped = true;
70            if index == bars_count {
71                options.last = true;
72            }
73        }
74
75        let pb = ProgressBar { bar, options };
76        self.bars.write().unwrap().insert(index, pb.clone());
77        pb
78    }
79
80    /// Returns the number of progress bars in the [`MultiProgress`].
81    pub fn length(&self) -> usize {
82        self.bars.read().unwrap().len()
83    }
84
85    /// Prints a log line above the multi-progress bar.
86    ///
87    /// By default, there is no empty line between each log added with
88    /// this function. To add an empty line, use a line
89    /// return character (`\n`) at the end of the message.
90    pub fn println(&self, message: impl Display) {
91        let theme = THEME.read().unwrap();
92        let symbol = theme.remark_symbol();
93        let log = theme.format_log_with_spacing(&message.to_string(), &symbol, false);
94        self.logs.fetch_add(log.lines().count(), Ordering::SeqCst);
95        self.multi.println(log).ok();
96    }
97
98    /// Stops the multi-progress bar with a submitted (successful) state.
99    pub fn stop(&self) {
100        self.stop_with(&ThemeState::Submit)
101    }
102
103    /// Stops the multi-progress bar with a default cancel message.
104    pub fn cancel(&self) {
105        self.stop_with(&ThemeState::Cancel)
106    }
107
108    /// Stops the multi-progress bar with an error message.
109    pub fn error(&self, error: impl Display) {
110        self.stop_with(&ThemeState::Error(error.to_string()))
111    }
112
113    fn stop_with(&self, state: &ThemeState) {
114        let mut inner_height = self.logs.load(Ordering::SeqCst);
115
116        // Redraw all progress bars.
117        for pb in self.bars.read().unwrap().iter() {
118            // Workaround: `bar.println` must be called before `bar.finish_and_clear`
119            // to avoid lines "jumping" while terminal resizing.
120            inner_height += pb.redraw_finished(pb.bar.message(), state);
121            pb.bar.finish_and_clear();
122        }
123
124        let term = Term::stderr();
125
126        // Move up to the header, clear and print the new header, then move down.
127        term.move_cursor_up(inner_height).ok();
128        term.clear_last_lines(HEADER_HEIGHT).ok();
129        term.write_str(
130            &THEME
131                .read()
132                .unwrap()
133                .format_header(state, (self.prompt.clone() + "\n ").trim_end()),
134        )
135        .ok();
136        term.move_cursor_down(inner_height).ok();
137    }
138}