1 use console::{measure_text_width, Style};
2 
3 use crate::format::{BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanDuration};
4 use crate::state::ProgressState;
5 use crate::utils::{expand_template, pad_str};
6 use std::borrow::Cow;
7 
8 #[cfg(feature = "improved_unicode")]
9 use unicode_segmentation::UnicodeSegmentation;
10 
11 /// Behavior of a progress bar when it is finished
12 ///
13 /// This is invoked when a [`ProgressBar`] or [`ProgressBarIter`] completes and
14 /// [`ProgressBar::is_finished`] is false.
15 ///
16 /// [`ProgressBar`]: crate::ProgressBar
17 /// [`ProgressBarIter`]: crate::ProgressBarIter
18 /// [`ProgressBar::is_finished`]: crate::ProgressBar::is_finished
19 #[derive(Clone, Debug)]
20 pub enum ProgressFinish {
21     /// Finishes the progress bar and leaves the current message
22     ///
23     /// Same behavior as calling [`ProgressBar::finish()`](crate::ProgressBar::finish).
24     AndLeave,
25     /// Finishes the progress bar at current position and leaves the current message
26     ///
27     /// Same behavior as calling [`ProgressBar::finish_at_current_pos()`](crate::ProgressBar::finish_at_current_pos).
28     AtCurrentPos,
29     /// Finishes the progress bar and sets a message
30     ///
31     /// Same behavior as calling [`ProgressBar::finish_with_message()`](crate::ProgressBar::finish_with_message).
32     WithMessage(Cow<'static, str>),
33     /// Finishes the progress bar and completely clears it (this is the default)
34     ///
35     /// Same behavior as calling [`ProgressBar::finish_and_clear()`](crate::ProgressBar::finish_and_clear).
36     AndClear,
37     /// Finishes the progress bar and leaves the current message and progress
38     ///
39     /// Same behavior as calling [`ProgressBar::abandon()`](crate::ProgressBar::abandon).
40     Abandon,
41     /// Finishes the progress bar and sets a message, and leaves the current progress
42     ///
43     /// Same behavior as calling [`ProgressBar::abandon_with_message()`](crate::ProgressBar::abandon_with_message).
44     AbandonWithMessage(Cow<'static, str>),
45 }
46 
47 impl Default for ProgressFinish {
default() -> Self48     fn default() -> Self {
49         Self::AndClear
50     }
51 }
52 
53 /// Controls the rendering style of progress bars
54 #[derive(Clone, Debug)]
55 pub struct ProgressStyle {
56     tick_strings: Vec<Box<str>>,
57     progress_chars: Vec<Box<str>>,
58     template: Box<str>,
59     on_finish: ProgressFinish,
60     // how unicode-big each char in progress_chars is
61     char_width: usize,
62 }
63 
64 #[cfg(feature = "improved_unicode")]
segment(s: &str) -> Vec<Box<str>>65 fn segment(s: &str) -> Vec<Box<str>> {
66     UnicodeSegmentation::graphemes(s, true)
67         .map(|s| s.into())
68         .collect()
69 }
70 
71 #[cfg(not(feature = "improved_unicode"))]
segment(s: &str) -> Vec<Box<str>>72 fn segment(s: &str) -> Vec<Box<str>> {
73     s.chars().map(|x| x.to_string().into()).collect()
74 }
75 
76 #[cfg(feature = "improved_unicode")]
measure(s: &str) -> usize77 fn measure(s: &str) -> usize {
78     unicode_width::UnicodeWidthStr::width(s)
79 }
80 
81 #[cfg(not(feature = "improved_unicode"))]
measure(s: &str) -> usize82 fn measure(s: &str) -> usize {
83     s.chars().count()
84 }
85 
86 /// finds the unicode-aware width of the passed grapheme cluters
87 /// panics on an empty parameter, or if the characters are not equal-width
width(c: &[Box<str>]) -> usize88 fn width(c: &[Box<str>]) -> usize {
89     c.iter()
90         .map(|s| measure(s.as_ref()))
91         .fold(None, |acc, new| {
92             match acc {
93                 None => return Some(new),
94                 Some(old) => assert_eq!(old, new, "got passed un-equal width progress characters"),
95             }
96             acc
97         })
98         .unwrap()
99 }
100 
101 impl ProgressStyle {
102     /// Returns the default progress bar style for bars
default_bar() -> ProgressStyle103     pub fn default_bar() -> ProgressStyle {
104         let progress_chars = segment("█░");
105         let char_width = width(&progress_chars);
106         ProgressStyle {
107             tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ "
108                 .chars()
109                 .map(|c| c.to_string().into())
110                 .collect(),
111             progress_chars,
112             char_width,
113             template: "{wide_bar} {pos}/{len}".into(),
114             on_finish: ProgressFinish::default(),
115         }
116     }
117 
118     /// Returns the default progress bar style for spinners
default_spinner() -> ProgressStyle119     pub fn default_spinner() -> ProgressStyle {
120         let progress_chars = segment("█░");
121         let char_width = width(&progress_chars);
122         ProgressStyle {
123             tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ "
124                 .chars()
125                 .map(|c| c.to_string().into())
126                 .collect(),
127             progress_chars,
128             char_width,
129             template: "{spinner} {msg}".into(),
130             on_finish: ProgressFinish::default(),
131         }
132     }
133 
134     /// Sets the tick character sequence for spinners
tick_chars(mut self, s: &str) -> ProgressStyle135     pub fn tick_chars(mut self, s: &str) -> ProgressStyle {
136         self.tick_strings = s.chars().map(|c| c.to_string().into()).collect();
137         // Format bar will panic with some potentially confusing message, better to panic here
138         // with a message explicitly informing of the problem
139         assert!(
140             self.tick_strings.len() >= 2,
141             "at least 2 tick chars required"
142         );
143         self
144     }
145 
146     /// Sets the tick string sequence for spinners
tick_strings(mut self, s: &[&str]) -> ProgressStyle147     pub fn tick_strings(mut self, s: &[&str]) -> ProgressStyle {
148         self.tick_strings = s.iter().map(|s| s.to_string().into()).collect();
149         // Format bar will panic with some potentially confusing message, better to panic here
150         // with a message explicitly informing of the problem
151         assert!(
152             self.progress_chars.len() >= 2,
153             "at least 2 tick strings required"
154         );
155         self
156     }
157 
158     /// Sets the progress characters `(filled, current, to do)`
159     ///
160     /// You can pass more then three for a more detailed display.
161     /// All passed grapheme clusters need to be of equal width.
progress_chars(mut self, s: &str) -> ProgressStyle162     pub fn progress_chars(mut self, s: &str) -> ProgressStyle {
163         self.progress_chars = segment(s);
164         // Format bar will panic with some potentially confusing message, better to panic here
165         // with a message explicitly informing of the problem
166         assert!(
167             self.progress_chars.len() >= 2,
168             "at least 2 progress chars required"
169         );
170         self.char_width = width(&self.progress_chars);
171         self
172     }
173 
174     /// Sets the template string for the progress bar
175     ///
176     /// Review the [list of template keys](./index.html#templates) for more information.
template(mut self, s: &str) -> ProgressStyle177     pub fn template(mut self, s: &str) -> ProgressStyle {
178         self.template = s.into();
179         self
180     }
181 
182     /// Sets the finish behavior for the progress bar
183     ///
184     /// This behavior is invoked when [`ProgressBar`] or
185     /// [`ProgressBarIter`] completes and
186     /// [`ProgressBar::is_finished()`] is false.
187     /// If you don't want the progress bar to be automatically finished then
188     /// call `on_finish(None)`.
189     ///
190     /// [`ProgressBar`]: crate::ProgressBar
191     /// [`ProgressBarIter`]: crate::ProgressBarIter
192     /// [`ProgressBar::is_finished()`]: crate::ProgressBar::is_finished
on_finish(mut self, finish: ProgressFinish) -> ProgressStyle193     pub fn on_finish(mut self, finish: ProgressFinish) -> ProgressStyle {
194         self.on_finish = finish;
195         self
196     }
197 
198     /// Returns the tick char for a given number
199     #[deprecated(since = "0.13.0", note = "Deprecated in favor of get_tick_str")]
get_tick_char(&self, idx: u64) -> char200     pub fn get_tick_char(&self, idx: u64) -> char {
201         self.get_tick_str(idx).chars().next().unwrap_or(' ')
202     }
203 
204     /// Returns the tick string for a given number
get_tick_str(&self, idx: u64) -> &str205     pub fn get_tick_str(&self, idx: u64) -> &str {
206         &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)]
207     }
208 
209     /// Returns the tick char for the finished state
210     #[deprecated(since = "0.13.0", note = "Deprecated in favor of get_final_tick_str")]
get_final_tick_char(&self) -> char211     pub fn get_final_tick_char(&self) -> char {
212         self.get_final_tick_str().chars().next().unwrap_or(' ')
213     }
214 
215     /// Returns the tick string for the finished state
get_final_tick_str(&self) -> &str216     pub fn get_final_tick_str(&self) -> &str {
217         &self.tick_strings[self.tick_strings.len() - 1]
218     }
219 
220     /// Returns the finish behavior
get_on_finish(&self) -> &ProgressFinish221     pub fn get_on_finish(&self) -> &ProgressFinish {
222         &self.on_finish
223     }
224 
format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> String225     pub(crate) fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> String {
226         // The number of clusters from progress_chars to write (rounding down).
227         let width = width / self.char_width;
228         // The number of full clusters (including a fractional component for a partially-full one).
229         let fill = fract * width as f32;
230         // The number of entirely full clusters (by truncating `fill`).
231         let entirely_filled = fill as usize;
232         // 1 if the bar is not entirely empty or full (meaning we need to draw the "current"
233         // character between the filled and "to do" segment), 0 otherwise.
234         let head = if fill > 0.0 && entirely_filled < width {
235             1
236         } else {
237             0
238         };
239 
240         let pb = self.progress_chars[0].repeat(entirely_filled);
241 
242         let cur = if head == 1 {
243             // Number of fine-grained progress entries in progress_chars.
244             let n = self.progress_chars.len().saturating_sub(2);
245             let cur_char = if n <= 1 {
246                 // No fine-grained entries. 1 is the single "current" entry if we have one, the "to
247                 // do" entry if not.
248                 1
249             } else {
250                 // Pick a fine-grained entry, ranging from the last one (n) if the fractional part
251                 // of fill is 0 to the first one (1) if the fractional part of fill is almost 1.
252                 n.saturating_sub((fill.fract() * n as f32) as usize)
253             };
254             self.progress_chars[cur_char].to_string()
255         } else {
256             "".into()
257         };
258 
259         // Number of entirely empty clusters needed to fill the bar up to `width`.
260         let bg = width.saturating_sub(entirely_filled).saturating_sub(head);
261         let rest = self.progress_chars[self.progress_chars.len() - 1].repeat(bg);
262         format!(
263             "{}{}{}",
264             pb,
265             cur,
266             alt_style.unwrap_or(&Style::new()).apply_to(rest)
267         )
268     }
269 
format_state(&self, state: &ProgressState) -> Vec<String>270     pub(crate) fn format_state(&self, state: &ProgressState) -> Vec<String> {
271         let (pos, len) = state.position();
272         let mut rv = vec![];
273 
274         for line in self.template.lines() {
275             let mut wide_element = None;
276 
277             let s = expand_template(line, |var| match var.key {
278                 "wide_bar" => {
279                     wide_element = Some(var.duplicate_for_key("bar"));
280                     "\x00".into()
281                 }
282                 "bar" => self.format_bar(
283                     state.fraction(),
284                     var.width.unwrap_or(20),
285                     var.alt_style.as_ref(),
286                 ),
287                 "spinner" => state.current_tick_str().to_string(),
288                 "wide_msg" => {
289                     wide_element = Some(var.duplicate_for_key("msg"));
290                     "\x00".into()
291                 }
292                 "msg" => state.message().to_string(),
293                 "prefix" => state.prefix().to_string(),
294                 "pos" => pos.to_string(),
295                 "len" => len.to_string(),
296                 "percent" => format!("{:.*}", 0, state.fraction() * 100f32),
297                 "bytes" => format!("{}", HumanBytes(state.pos)),
298                 "total_bytes" => format!("{}", HumanBytes(state.len)),
299                 "decimal_bytes" => format!("{}", DecimalBytes(state.pos)),
300                 "decimal_total_bytes" => format!("{}", DecimalBytes(state.len)),
301                 "binary_bytes" => format!("{}", BinaryBytes(state.pos)),
302                 "binary_total_bytes" => format!("{}", BinaryBytes(state.len)),
303                 "elapsed_precise" => format!("{}", FormattedDuration(state.started.elapsed())),
304                 "elapsed" => format!("{:#}", HumanDuration(state.started.elapsed())),
305                 "per_sec" => format!("{}/s", state.per_sec()),
306                 "bytes_per_sec" => format!("{}/s", HumanBytes(state.per_sec())),
307                 "binary_bytes_per_sec" => format!("{}/s", BinaryBytes(state.per_sec())),
308                 "eta_precise" => format!("{}", FormattedDuration(state.eta())),
309                 "eta" => format!("{:#}", HumanDuration(state.eta())),
310                 "duration_precise" => format!("{}", FormattedDuration(state.duration())),
311                 "duration" => format!("{:#}", HumanDuration(state.duration())),
312                 _ => "".into(),
313             });
314 
315             rv.push(if let Some(ref var) = wide_element {
316                 let total_width = state.width();
317                 if var.key == "bar" {
318                     let bar_width = total_width.saturating_sub(measure_text_width(&s));
319                     s.replace(
320                         "\x00",
321                         &self.format_bar(state.fraction(), bar_width, var.alt_style.as_ref()),
322                     )
323                 } else if var.key == "msg" {
324                     let msg_width = total_width.saturating_sub(measure_text_width(&s));
325                     let msg = pad_str(state.message(), msg_width, var.align, true);
326                     s.replace(
327                         "\x00",
328                         if var.last_element {
329                             msg.trim_end()
330                         } else {
331                             &msg
332                         },
333                     )
334                 } else {
335                     unreachable!()
336                 }
337             } else {
338                 s.to_string()
339             });
340         }
341 
342         rv
343     }
344 }
345