kdam/rich/
bar.rs

1use super::styles;
2use crate::{BarExt, std::Bar, term::Colorizer};
3use std::num::{NonZeroI16, NonZeroU16};
4
5#[cfg(feature = "spinner")]
6use crate::spinner::Spinner;
7
8/// Renderable columns for [RichProgress](RichProgress).
9#[derive(Debug, Clone)]
10pub enum Column {
11    /// Progress bar animation display.
12    ///
13    /// If `total = 0`, a pulsating animation is shown else a normal animation is shown.
14    Animation,
15    /// Progress counter display.
16    Count,
17    /// Progress formatted counter display i.e. `counter/total`.
18    CountTotal,
19    /// Progress elapsed time display.
20    ElapsedTime,
21    /// Progress percentage done (with precision) display.
22    Percentage(usize),
23    /// Progress update rate display.
24    Rate,
25    /// Progress remaining time (ETA) display.
26    RemainingTime,
27    /// Custom spinners display.
28    #[cfg(feature = "spinner")]
29    #[cfg_attr(docsrs, doc(cfg(feature = "spinner")))]
30    Spinner(Spinner),
31    /// Custom text display.
32    ///
33    /// # Example
34    ///
35    /// ```
36    /// use kdam::Column;
37    ///
38    /// Column::Text("•".to_owned());
39    /// Column::Text("[bold red]Downloading".to_owned());
40    /// ```
41    Text(String),
42    /// Progress total display.
43    Total,
44}
45
46/// An implementation [rich.progress](https://rich.readthedocs.io/en/latest/progress.html) using [Bar](crate::Bar).
47///
48/// # Example
49///
50/// ```
51/// use kdam::{tqdm, Column, BarExt, RichProgress};
52///
53/// let mut pb = RichProgress::new(
54///     tqdm!(total = 100),
55///     vec![Column::Animation, Column::Percentage(2)]
56/// );
57///
58/// for _ in 0..100 {
59///     pb.update(1).unwrap();
60/// }
61///
62/// eprintln!();
63/// ```
64#[derive(BarExt, Debug)]
65pub struct RichProgress {
66    pub columns: Vec<Column>,
67    #[bar]
68    pub pb: Bar,
69}
70
71impl RichProgress {
72    /// Create a new [RichProgress](Self).
73    pub fn new(pb: Bar, columns: Vec<Column>) -> Self {
74        Self { columns, pb }
75    }
76
77    /// Replace a column at specific index.
78    ///
79    /// # Panics
80    ///
81    /// If `index` is out of bounds.
82    pub fn replace(&mut self, index: usize, col: Column) {
83        *self.columns.get_mut(index).unwrap() = col;
84        // let _ = std::mem::replace(&mut self.columns[index], col);
85    }
86
87    /// Render progress bar text.
88    pub fn render(&mut self) -> String {
89        let mut bar_text = vec![];
90        let mut bar_length = 0;
91        let mut progress_bar_index = None;
92
93        for col in self.columns.iter() {
94            match col {
95                Column::Animation => {
96                    progress_bar_index = Some(bar_text.len());
97                    bar_text.push(String::new());
98                }
99
100                Column::Count => {
101                    let fmt_progress = self.pb.fmt_counter();
102                    bar_length += fmt_progress.len();
103                    bar_text.push(fmt_progress.colorize("green"));
104                }
105
106                Column::CountTotal => {
107                    let fmt_progress = format!("{}/{}", self.pb.fmt_counter(), self.pb.fmt_total());
108                    bar_length += fmt_progress.len();
109                    bar_text.push(fmt_progress.colorize("green"));
110                }
111
112                Column::ElapsedTime => {
113                    let elapsed_time = self.pb.fmt_elapsed_time();
114                    bar_length += elapsed_time.len();
115                    bar_text.push(elapsed_time.colorize("cyan"));
116                }
117
118                Column::Percentage(precision) => {
119                    let percentage = format!("{:.1$}%", self.pb.percentage() * 100., precision);
120                    bar_length += percentage.len();
121                    bar_text.push(percentage.colorize("magenta"));
122                }
123
124                Column::Rate => {
125                    let rate = self.pb.fmt_rate();
126                    bar_length += rate.len();
127                    bar_text.push(rate.colorize("red"));
128                }
129
130                Column::RemainingTime => {
131                    let remaining_time = self.pb.fmt_remaining_time();
132                    bar_length += remaining_time.len();
133                    bar_text.push(remaining_time.colorize("cyan"));
134                }
135
136                #[cfg(feature = "spinner")]
137                Column::Spinner(spinner) => {
138                    let frame = spinner.render_frame(self.pb.elapsed_time());
139                    bar_length += frame.chars().count();
140                    bar_text.push(frame.colorize("green"));
141                }
142
143                Column::Text(text) => {
144                    let (color, text_start_index) = match (text.find('['), text.find(']')) {
145                        (Some(start), Some(end)) => {
146                            if start == 0 {
147                                (text.get(1..end), end + 1)
148                            } else {
149                                (None, 0)
150                            }
151                        }
152                        _ => (None, 0),
153                    };
154
155                    if let Some(code) = color {
156                        let text = &text[text_start_index..];
157                        bar_length += text.len_ansi();
158                        bar_text.push(text.colorize(code));
159                    } else {
160                        bar_length += text.len_ansi();
161                        bar_text.push(text.to_owned());
162                    }
163                }
164
165                Column::Total => {
166                    let fmt_progress = self.pb.fmt_total();
167                    bar_length += fmt_progress.len();
168                    bar_text.push(fmt_progress.colorize("green"));
169                }
170            }
171        }
172
173        bar_length += bar_text.len() - 1;
174        let mut ncols = 0;
175
176        if let Some(progress_bar_index) = progress_bar_index {
177            ncols = self.pb.ncols_for_animation(bar_length as u16);
178
179            if ncols == 0 {
180                let _ = bar_text.remove(progress_bar_index);
181            } else {
182                *bar_text.get_mut(progress_bar_index).unwrap() =
183                    if self.pb.indefinite() || !self.pb.started() {
184                        styles::pulse(
185                            NonZeroI16::new(ncols as i16).unwrap(),
186                            self.pb.elapsed_time(),
187                        )
188                    } else {
189                        styles::bar(NonZeroU16::new(ncols).unwrap(), self.pb.percentage())
190                    };
191            }
192        }
193
194        self.pb.bar_length = ncols + bar_length as u16;
195        bar_text.join(" ")
196    }
197}