1use std::collections::HashMap;
2use std::fmt::{self, Write};
3use std::mem;
4#[cfg(not(target_arch = "wasm32"))]
5use std::time::Instant;
6
7use console::{measure_text_width, Style};
8#[cfg(target_arch = "wasm32")]
9use instant::Instant;
10#[cfg(feature = "unicode-segmentation")]
11use unicode_segmentation::UnicodeSegmentation;
12
13use crate::format::{
14 BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
15 HumanFloatCount,
16};
17use crate::state::{ProgressState, TabExpandedString, DEFAULT_TAB_WIDTH};
18
19#[derive(Clone)]
20pub struct ProgressStyle {
21 tick_strings: Vec<Box<str>>,
22 progress_chars: Vec<Box<str>>,
23 template: Template,
24 // how unicode-big each char in progress_chars is
25 char_width: usize,
26 tab_width: usize,
27 pub(crate) format_map: HashMap<&'static str, Box<dyn ProgressTracker>>,
28}
29
30#[cfg(feature = "unicode-segmentation")]
31fn segment(s: &str) -> Vec<Box<str>> {
32 UnicodeSegmentation::graphemes(s, true)
33 .map(|s| s.into())
34 .collect()
35}
36
37#[cfg(not(feature = "unicode-segmentation"))]
38fn segment(s: &str) -> Vec<Box<str>> {
39 s.chars().map(|x: char| x.to_string().into()).collect()
40}
41
42#[cfg(feature = "unicode-width")]
43fn measure(s: &str) -> usize {
44 unicode_width::UnicodeWidthStr::width(self:s)
45}
46
47#[cfg(not(feature = "unicode-width"))]
48fn measure(s: &str) -> usize {
49 s.chars().count()
50}
51
52/// finds the unicode-aware width of the passed grapheme cluters
53/// panics on an empty parameter, or if the characters are not equal-width
54fn width(c: &[Box<str>]) -> usize {
55 cOption.iter()
56 .map(|s| measure(s.as_ref()))
57 .fold(init:None, |acc: Option, new: usize| {
58 match acc {
59 None => return Some(new),
60 Some(old: usize) => assert_eq!(old, new, "got passed un-equal width progress characters"),
61 }
62 acc
63 })
64 .unwrap()
65}
66
67impl ProgressStyle {
68 /// Returns the default progress bar style for bars
69 pub fn default_bar() -> Self {
70 Self::new(Template::from_str("{wide_bar} {pos}/{len}").unwrap())
71 }
72
73 /// Returns the default progress bar style for spinners
74 pub fn default_spinner() -> Self {
75 Self::new(Template::from_str("{spinner} {msg}").unwrap())
76 }
77
78 /// Sets the template string for the progress bar
79 ///
80 /// Review the [list of template keys](../index.html#templates) for more information.
81 pub fn with_template(template: &str) -> Result<Self, TemplateError> {
82 Ok(Self::new(Template::from_str(template)?))
83 }
84
85 pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) {
86 self.tab_width = new_tab_width;
87 self.template.set_tab_width(new_tab_width);
88 }
89
90 fn new(template: Template) -> Self {
91 let progress_chars = segment("█░");
92 let char_width = width(&progress_chars);
93 Self {
94 tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ "
95 .chars()
96 .map(|c| c.to_string().into())
97 .collect(),
98 progress_chars,
99 char_width,
100 template,
101 format_map: HashMap::default(),
102 tab_width: DEFAULT_TAB_WIDTH,
103 }
104 }
105
106 /// Sets the tick character sequence for spinners
107 ///
108 /// Note that the last character is used as the [final tick string][Self::get_final_tick_str()].
109 /// At least two characters are required to provide a non-final and final state.
110 pub fn tick_chars(mut self, s: &str) -> Self {
111 self.tick_strings = s.chars().map(|c| c.to_string().into()).collect();
112 // Format bar will panic with some potentially confusing message, better to panic here
113 // with a message explicitly informing of the problem
114 assert!(
115 self.tick_strings.len() >= 2,
116 "at least 2 tick chars required"
117 );
118 self
119 }
120
121 /// Sets the tick string sequence for spinners
122 ///
123 /// Note that the last string is used as the [final tick string][Self::get_final_tick_str()].
124 /// At least two strings are required to provide a non-final and final state.
125 pub fn tick_strings(mut self, s: &[&str]) -> Self {
126 self.tick_strings = s.iter().map(|s| s.to_string().into()).collect();
127 // Format bar will panic with some potentially confusing message, better to panic here
128 // with a message explicitly informing of the problem
129 assert!(
130 self.progress_chars.len() >= 2,
131 "at least 2 tick strings required"
132 );
133 self
134 }
135
136 /// Sets the progress characters `(filled, current, to do)`
137 ///
138 /// You can pass more than three for a more detailed display.
139 /// All passed grapheme clusters need to be of equal width.
140 pub fn progress_chars(mut self, s: &str) -> Self {
141 self.progress_chars = segment(s);
142 // Format bar will panic with some potentially confusing message, better to panic here
143 // with a message explicitly informing of the problem
144 assert!(
145 self.progress_chars.len() >= 2,
146 "at least 2 progress chars required"
147 );
148 self.char_width = width(&self.progress_chars);
149 self
150 }
151
152 /// Adds a custom key that owns a [`ProgressTracker`] to the template
153 pub fn with_key<S: ProgressTracker + 'static>(mut self, key: &'static str, f: S) -> Self {
154 self.format_map.insert(key, Box::new(f));
155 self
156 }
157
158 /// Sets the template string for the progress bar
159 ///
160 /// Review the [list of template keys](../index.html#templates) for more information.
161 pub fn template(mut self, s: &str) -> Result<Self, TemplateError> {
162 self.template = Template::from_str(s)?;
163 Ok(self)
164 }
165
166 fn current_tick_str(&self, state: &ProgressState) -> &str {
167 match state.is_finished() {
168 true => self.get_final_tick_str(),
169 false => self.get_tick_str(state.tick),
170 }
171 }
172
173 /// Returns the tick string for a given number
174 pub fn get_tick_str(&self, idx: u64) -> &str {
175 &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)]
176 }
177
178 /// Returns the tick string for the finished state
179 pub fn get_final_tick_str(&self) -> &str {
180 &self.tick_strings[self.tick_strings.len() - 1]
181 }
182
183 fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> BarDisplay<'_> {
184 // The number of clusters from progress_chars to write (rounding down).
185 let width = width / self.char_width;
186 // The number of full clusters (including a fractional component for a partially-full one).
187 let fill = fract * width as f32;
188 // The number of entirely full clusters (by truncating `fill`).
189 let entirely_filled = fill as usize;
190 // 1 if the bar is not entirely empty or full (meaning we need to draw the "current"
191 // character between the filled and "to do" segment), 0 otherwise.
192 let head = usize::from(fill > 0.0 && entirely_filled < width);
193
194 let cur = if head == 1 {
195 // Number of fine-grained progress entries in progress_chars.
196 let n = self.progress_chars.len().saturating_sub(2);
197 let cur_char = if n <= 1 {
198 // No fine-grained entries. 1 is the single "current" entry if we have one, the "to
199 // do" entry if not.
200 1
201 } else {
202 // Pick a fine-grained entry, ranging from the last one (n) if the fractional part
203 // of fill is 0 to the first one (1) if the fractional part of fill is almost 1.
204 n.saturating_sub((fill.fract() * n as f32) as usize)
205 };
206 Some(cur_char)
207 } else {
208 None
209 };
210
211 // Number of entirely empty clusters needed to fill the bar up to `width`.
212 let bg = width.saturating_sub(entirely_filled).saturating_sub(head);
213 let rest = RepeatedStringDisplay {
214 str: &self.progress_chars[self.progress_chars.len() - 1],
215 num: bg,
216 };
217
218 BarDisplay {
219 chars: &self.progress_chars,
220 filled: entirely_filled,
221 cur,
222 rest: alt_style.unwrap_or(&Style::new()).apply_to(rest),
223 }
224 }
225
226 pub(crate) fn format_state(
227 &self,
228 state: &ProgressState,
229 lines: &mut Vec<String>,
230 target_width: u16,
231 ) {
232 let mut cur = String::new();
233 let mut buf = String::new();
234 let mut wide = None;
235
236 let pos = state.pos();
237 let len = state.len().unwrap_or(pos);
238 for part in &self.template.parts {
239 match part {
240 TemplatePart::Placeholder {
241 key,
242 align,
243 width,
244 truncate,
245 style,
246 alt_style,
247 } => {
248 buf.clear();
249 if let Some(tracker) = self.format_map.get(key.as_str()) {
250 tracker.write(state, &mut TabRewriter(&mut buf, self.tab_width));
251 } else {
252 match key.as_str() {
253 "wide_bar" => {
254 wide = Some(WideElement::Bar { alt_style });
255 buf.push('\x00');
256 }
257 "bar" => buf
258 .write_fmt(format_args!(
259 "{}",
260 self.format_bar(
261 state.fraction(),
262 width.unwrap_or(20) as usize,
263 alt_style.as_ref(),
264 )
265 ))
266 .unwrap(),
267 "spinner" => buf.push_str(self.current_tick_str(state)),
268 "wide_msg" => {
269 wide = Some(WideElement::Message { align });
270 buf.push('\x00');
271 }
272 "msg" => buf.push_str(state.message.expanded()),
273 "prefix" => buf.push_str(state.prefix.expanded()),
274 "pos" => buf.write_fmt(format_args!("{pos}")).unwrap(),
275 "human_pos" => {
276 buf.write_fmt(format_args!("{}", HumanCount(pos))).unwrap();
277 }
278 "len" => buf.write_fmt(format_args!("{len}")).unwrap(),
279 "human_len" => {
280 buf.write_fmt(format_args!("{}", HumanCount(len))).unwrap();
281 }
282 "percent" => buf
283 .write_fmt(format_args!("{:.*}", 0, state.fraction() * 100f32))
284 .unwrap(),
285 "bytes" => buf.write_fmt(format_args!("{}", HumanBytes(pos))).unwrap(),
286 "total_bytes" => {
287 buf.write_fmt(format_args!("{}", HumanBytes(len))).unwrap();
288 }
289 "decimal_bytes" => buf
290 .write_fmt(format_args!("{}", DecimalBytes(pos)))
291 .unwrap(),
292 "decimal_total_bytes" => buf
293 .write_fmt(format_args!("{}", DecimalBytes(len)))
294 .unwrap(),
295 "binary_bytes" => {
296 buf.write_fmt(format_args!("{}", BinaryBytes(pos))).unwrap();
297 }
298 "binary_total_bytes" => {
299 buf.write_fmt(format_args!("{}", BinaryBytes(len))).unwrap();
300 }
301 "elapsed_precise" => buf
302 .write_fmt(format_args!("{}", FormattedDuration(state.elapsed())))
303 .unwrap(),
304 "elapsed" => buf
305 .write_fmt(format_args!("{:#}", HumanDuration(state.elapsed())))
306 .unwrap(),
307 "per_sec" => buf
308 .write_fmt(format_args!("{}/s", HumanFloatCount(state.per_sec())))
309 .unwrap(),
310 "bytes_per_sec" => buf
311 .write_fmt(format_args!("{}/s", HumanBytes(state.per_sec() as u64)))
312 .unwrap(),
313 "binary_bytes_per_sec" => buf
314 .write_fmt(format_args!(
315 "{}/s",
316 BinaryBytes(state.per_sec() as u64)
317 ))
318 .unwrap(),
319 "eta_precise" => buf
320 .write_fmt(format_args!("{}", FormattedDuration(state.eta())))
321 .unwrap(),
322 "eta" => buf
323 .write_fmt(format_args!("{:#}", HumanDuration(state.eta())))
324 .unwrap(),
325 "duration_precise" => buf
326 .write_fmt(format_args!("{}", FormattedDuration(state.duration())))
327 .unwrap(),
328 "duration" => buf
329 .write_fmt(format_args!("{:#}", HumanDuration(state.duration())))
330 .unwrap(),
331 _ => (),
332 }
333 };
334
335 match width {
336 Some(width) => {
337 let padded = PaddedStringDisplay {
338 str: &buf,
339 width: *width as usize,
340 align: *align,
341 truncate: *truncate,
342 };
343 match style {
344 Some(s) => cur
345 .write_fmt(format_args!("{}", s.apply_to(padded)))
346 .unwrap(),
347 None => cur.write_fmt(format_args!("{padded}")).unwrap(),
348 }
349 }
350 None => match style {
351 Some(s) => cur.write_fmt(format_args!("{}", s.apply_to(&buf))).unwrap(),
352 None => cur.push_str(&buf),
353 },
354 }
355 }
356 TemplatePart::Literal(s) => cur.push_str(s.expanded()),
357 TemplatePart::NewLine => {
358 self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
359 }
360 }
361 }
362
363 if !cur.is_empty() {
364 self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
365 }
366 }
367
368 fn push_line(
369 &self,
370 lines: &mut Vec<String>,
371 cur: &mut String,
372 state: &ProgressState,
373 buf: &mut String,
374 target_width: u16,
375 wide: &Option<WideElement>,
376 ) {
377 let expanded = match wide {
378 Some(inner) => inner.expand(mem::take(cur), self, state, buf, target_width),
379 None => mem::take(cur),
380 };
381
382 // If there are newlines, we need to split them up
383 // and add the lines separately so that they're counted
384 // correctly on re-render.
385 for (i, line) in expanded.split('\n').enumerate() {
386 // No newlines found in this case
387 if i == 0 && line.len() == expanded.len() {
388 lines.push(expanded);
389 break;
390 }
391
392 lines.push(line.to_string());
393 }
394 }
395}
396
397struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize);
398
399impl Write for TabRewriter<'_> {
400 fn write_str(&mut self, s: &str) -> fmt::Result {
401 self.0
402 .write_str(s.replace(from:'\t', &" ".repeat(self.1)).as_str())
403 }
404}
405
406#[derive(Clone, Copy)]
407enum WideElement<'a> {
408 Bar { alt_style: &'a Option<Style> },
409 Message { align: &'a Alignment },
410}
411
412impl<'a> WideElement<'a> {
413 fn expand(
414 self,
415 cur: String,
416 style: &ProgressStyle,
417 state: &ProgressState,
418 buf: &mut String,
419 width: u16,
420 ) -> String {
421 let left = (width as usize).saturating_sub(measure_text_width(&cur.replace('\x00', "")));
422 match self {
423 Self::Bar { alt_style } => cur.replace(
424 '\x00',
425 &format!(
426 "{}",
427 style.format_bar(state.fraction(), left, alt_style.as_ref())
428 ),
429 ),
430 WideElement::Message { align } => {
431 buf.clear();
432 buf.write_fmt(format_args!(
433 "{}",
434 PaddedStringDisplay {
435 str: state.message.expanded(),
436 width: left,
437 align: *align,
438 truncate: true,
439 }
440 ))
441 .unwrap();
442
443 let trimmed = match cur.as_bytes().last() == Some(&b'\x00') {
444 true => buf.trim_end(),
445 false => buf,
446 };
447
448 cur.replace('\x00', trimmed)
449 }
450 }
451 }
452}
453
454#[derive(Clone, Debug)]
455struct Template {
456 parts: Vec<TemplatePart>,
457}
458
459impl Template {
460 fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> {
461 use State::*;
462 let (mut state, mut parts, mut buf) = (Literal, vec![], String::new());
463 for c in s.chars() {
464 let new = match (state, c) {
465 (Literal, '{') => (MaybeOpen, None),
466 (Literal, '\n') => {
467 if !buf.is_empty() {
468 parts.push(TemplatePart::Literal(TabExpandedString::new(
469 mem::take(&mut buf).into(),
470 tab_width,
471 )));
472 }
473 parts.push(TemplatePart::NewLine);
474 (Literal, None)
475 }
476 (Literal, '}') => (DoubleClose, Some('}')),
477 (Literal, c) => (Literal, Some(c)),
478 (DoubleClose, '}') => (Literal, None),
479 (MaybeOpen, '{') => (Literal, Some('{')),
480 (MaybeOpen | Key, c) if c.is_ascii_whitespace() => {
481 // If we find whitespace where the variable key is supposed to go,
482 // backtrack and act as if this was a literal.
483 buf.push(c);
484 let mut new = String::from("{");
485 new.push_str(&buf);
486 buf.clear();
487 parts.push(TemplatePart::Literal(TabExpandedString::new(
488 new.into(),
489 tab_width,
490 )));
491 (Literal, None)
492 }
493 (MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)),
494 (Key, c) if c != '}' && c != ':' => (Key, Some(c)),
495 (Key, ':') => (Align, None),
496 (Key, '}') => (Literal, None),
497 (Key, '!') if !buf.is_empty() => {
498 parts.push(TemplatePart::Placeholder {
499 key: mem::take(&mut buf),
500 align: Alignment::Left,
501 width: None,
502 truncate: true,
503 style: None,
504 alt_style: None,
505 });
506 (Width, None)
507 }
508 (Align, c) if c == '<' || c == '^' || c == '>' => {
509 if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() {
510 match c {
511 '<' => *align = Alignment::Left,
512 '^' => *align = Alignment::Center,
513 '>' => *align = Alignment::Right,
514 _ => (),
515 }
516 }
517
518 (Width, None)
519 }
520 (Align, c @ '0'..='9') => (Width, Some(c)),
521 (Align | Width, '!') => {
522 if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() {
523 *truncate = true;
524 }
525 (Width, None)
526 }
527 (Align, '.') => (FirstStyle, None),
528 (Align, '}') => (Literal, None),
529 (Width, c @ '0'..='9') => (Width, Some(c)),
530 (Width, '.') => (FirstStyle, None),
531 (Width, '}') => (Literal, None),
532 (FirstStyle, '/') => (AltStyle, None),
533 (FirstStyle, '}') => (Literal, None),
534 (FirstStyle, c) => (FirstStyle, Some(c)),
535 (AltStyle, '}') => (Literal, None),
536 (AltStyle, c) => (AltStyle, Some(c)),
537 (st, c) => return Err(TemplateError { next: c, state: st }),
538 };
539
540 match (state, new.0) {
541 (MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal(
542 TabExpandedString::new(mem::take(&mut buf).into(), tab_width),
543 )),
544 (Key, Align | Literal) if !buf.is_empty() => {
545 parts.push(TemplatePart::Placeholder {
546 key: mem::take(&mut buf),
547 align: Alignment::Left,
548 width: None,
549 truncate: false,
550 style: None,
551 alt_style: None,
552 });
553 }
554 (Width, FirstStyle | Literal) if !buf.is_empty() => {
555 if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() {
556 *width = Some(buf.parse().unwrap());
557 buf.clear();
558 }
559 }
560 (FirstStyle, AltStyle | Literal) if !buf.is_empty() => {
561 if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() {
562 *style = Some(Style::from_dotted_str(&buf));
563 buf.clear();
564 }
565 }
566 (AltStyle, Literal) if !buf.is_empty() => {
567 if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() {
568 *alt_style = Some(Style::from_dotted_str(&buf));
569 buf.clear();
570 }
571 }
572 (_, _) => (),
573 }
574
575 state = new.0;
576 if let Some(c) = new.1 {
577 buf.push(c);
578 }
579 }
580
581 if matches!(state, Literal | DoubleClose) && !buf.is_empty() {
582 parts.push(TemplatePart::Literal(TabExpandedString::new(
583 buf.into(),
584 tab_width,
585 )));
586 }
587
588 Ok(Self { parts })
589 }
590
591 fn from_str(s: &str) -> Result<Self, TemplateError> {
592 Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH)
593 }
594
595 fn set_tab_width(&mut self, new_tab_width: usize) {
596 for part in &mut self.parts {
597 if let TemplatePart::Literal(s) = part {
598 s.set_tab_width(new_tab_width);
599 }
600 }
601 }
602}
603
604#[derive(Debug)]
605pub struct TemplateError {
606 state: State,
607 next: char,
608}
609
610impl fmt::Display for TemplateError {
611 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612 write!(
613 f,
614 "TemplateError: unexpected character {:?} in state {:?}",
615 self.next, self.state
616 )
617 }
618}
619
620impl std::error::Error for TemplateError {}
621
622#[derive(Clone, Debug, PartialEq, Eq)]
623enum TemplatePart {
624 Literal(TabExpandedString),
625 Placeholder {
626 key: String,
627 align: Alignment,
628 width: Option<u16>,
629 truncate: bool,
630 style: Option<Style>,
631 alt_style: Option<Style>,
632 },
633 NewLine,
634}
635
636#[derive(Copy, Clone, Debug, PartialEq, Eq)]
637enum State {
638 Literal,
639 MaybeOpen,
640 DoubleClose,
641 Key,
642 Align,
643 Width,
644 FirstStyle,
645 AltStyle,
646}
647
648struct BarDisplay<'a> {
649 chars: &'a [Box<str>],
650 filled: usize,
651 cur: Option<usize>,
652 rest: console::StyledObject<RepeatedStringDisplay<'a>>,
653}
654
655impl<'a> fmt::Display for BarDisplay<'a> {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 for _ in 0..self.filled {
658 f.write_str(&self.chars[0])?;
659 }
660 if let Some(cur: usize) = self.cur {
661 f.write_str(&self.chars[cur])?;
662 }
663 self.rest.fmt(f)
664 }
665}
666
667struct RepeatedStringDisplay<'a> {
668 str: &'a str,
669 num: usize,
670}
671
672impl<'a> fmt::Display for RepeatedStringDisplay<'a> {
673 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674 for _ in 0..self.num {
675 f.write_str(self.str)?;
676 }
677 Ok(())
678 }
679}
680
681struct PaddedStringDisplay<'a> {
682 str: &'a str,
683 width: usize,
684 align: Alignment,
685 truncate: bool,
686}
687
688impl<'a> fmt::Display for PaddedStringDisplay<'a> {
689 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690 let cols = measure_text_width(self.str);
691 let excess = cols.saturating_sub(self.width);
692 if excess > 0 && !self.truncate {
693 return f.write_str(self.str);
694 } else if excess > 0 {
695 let (start, end) = match self.align {
696 Alignment::Left => (0, self.str.len() - excess),
697 Alignment::Right => (excess, self.str.len()),
698 Alignment::Center => (
699 excess / 2,
700 self.str.len() - excess.saturating_sub(excess / 2),
701 ),
702 };
703
704 return f.write_str(self.str.get(start..end).unwrap_or(self.str));
705 }
706
707 let diff = self.width.saturating_sub(cols);
708 let (left_pad, right_pad) = match self.align {
709 Alignment::Left => (0, diff),
710 Alignment::Right => (diff, 0),
711 Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)),
712 };
713
714 for _ in 0..left_pad {
715 f.write_char(' ')?;
716 }
717 f.write_str(self.str)?;
718 for _ in 0..right_pad {
719 f.write_char(' ')?;
720 }
721 Ok(())
722 }
723}
724
725#[derive(PartialEq, Eq, Debug, Copy, Clone)]
726enum Alignment {
727 Left,
728 Center,
729 Right,
730}
731
732/// Trait for defining stateful or stateless formatters
733pub trait ProgressTracker: Send + Sync {
734 /// Creates a new instance of the progress tracker
735 fn clone_box(&self) -> Box<dyn ProgressTracker>;
736 /// Notifies the progress tracker of a tick event
737 fn tick(&mut self, state: &ProgressState, now: Instant);
738 /// Notifies the progress tracker of a reset event
739 fn reset(&mut self, state: &ProgressState, now: Instant);
740 /// Provides access to the progress bar display buffer for custom messages
741 fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write);
742}
743
744impl Clone for Box<dyn ProgressTracker> {
745 fn clone(&self) -> Self {
746 self.clone_box()
747 }
748}
749
750impl<F> ProgressTracker for F
751where
752 F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Sync + Clone + 'static,
753{
754 fn clone_box(&self) -> Box<dyn ProgressTracker> {
755 Box::new(self.clone())
756 }
757
758 fn tick(&mut self, _: &ProgressState, _: Instant) {}
759
760 fn reset(&mut self, _: &ProgressState, _: Instant) {}
761
762 fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) {
763 (self)(state, w);
764 }
765}
766
767#[cfg(test)]
768mod tests {
769 use std::sync::Arc;
770
771 use super::*;
772 use crate::state::{AtomicPosition, ProgressState};
773 use std::sync::Mutex;
774
775 #[test]
776 fn test_stateful_tracker() {
777 #[derive(Debug, Clone)]
778 struct TestTracker(Arc<Mutex<String>>);
779
780 impl ProgressTracker for TestTracker {
781 fn clone_box(&self) -> Box<dyn ProgressTracker> {
782 Box::new(self.clone())
783 }
784
785 fn tick(&mut self, state: &ProgressState, _: Instant) {
786 let mut m = self.0.lock().unwrap();
787 m.clear();
788 m.push_str(format!("{} {}", state.len().unwrap(), state.pos()).as_str());
789 }
790
791 fn reset(&mut self, _state: &ProgressState, _: Instant) {
792 let mut m = self.0.lock().unwrap();
793 m.clear();
794 }
795
796 fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) {
797 w.write_str(self.0.lock().unwrap().as_str()).unwrap();
798 }
799 }
800
801 use crate::ProgressBar;
802
803 let pb = ProgressBar::new(1);
804 pb.set_style(
805 ProgressStyle::with_template("{{ {foo} }}")
806 .unwrap()
807 .with_key("foo", TestTracker(Arc::new(Mutex::new(String::default()))))
808 .progress_chars("#>-"),
809 );
810
811 let mut buf = Vec::new();
812 let style = pb.clone().style();
813
814 style.format_state(&pb.state().state, &mut buf, 16);
815 assert_eq!(&buf[0], "{ }");
816 buf.clear();
817 pb.inc(1);
818 style.format_state(&pb.state().state, &mut buf, 16);
819 assert_eq!(&buf[0], "{ 1 1 }");
820 pb.reset();
821 buf.clear();
822 style.format_state(&pb.state().state, &mut buf, 16);
823 assert_eq!(&buf[0], "{ }");
824 pb.finish_and_clear();
825 }
826
827 use crate::state::TabExpandedString;
828
829 #[test]
830 fn test_expand_template() {
831 const WIDTH: u16 = 80;
832 let pos = Arc::new(AtomicPosition::new());
833 let state = ProgressState::new(Some(10), pos);
834 let mut buf = Vec::new();
835
836 let mut style = ProgressStyle::default_bar();
837 style.format_map.insert(
838 "foo",
839 Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO").unwrap()),
840 );
841 style.format_map.insert(
842 "bar",
843 Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR").unwrap()),
844 );
845
846 style.template = Template::from_str("{{ {foo} {bar} }}").unwrap();
847 style.format_state(&state, &mut buf, WIDTH);
848 assert_eq!(&buf[0], "{ FOO BAR }");
849
850 buf.clear();
851 style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"#).unwrap();
852 style.format_state(&state, &mut buf, WIDTH);
853 assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"#);
854 }
855
856 #[test]
857 fn test_expand_template_flags() {
858 use console::set_colors_enabled;
859 set_colors_enabled(true);
860
861 const WIDTH: u16 = 80;
862 let pos = Arc::new(AtomicPosition::new());
863 let state = ProgressState::new(Some(10), pos);
864 let mut buf = Vec::new();
865
866 let mut style = ProgressStyle::default_bar();
867 style.format_map.insert(
868 "foo",
869 Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX").unwrap()),
870 );
871
872 style.template = Template::from_str("{foo:5}").unwrap();
873 style.format_state(&state, &mut buf, WIDTH);
874 assert_eq!(&buf[0], "XXX ");
875
876 buf.clear();
877 style.template = Template::from_str("{foo:.red.on_blue}").unwrap();
878 style.format_state(&state, &mut buf, WIDTH);
879 assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m");
880
881 buf.clear();
882 style.template = Template::from_str("{foo:^5.red.on_blue}").unwrap();
883 style.format_state(&state, &mut buf, WIDTH);
884 assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
885
886 buf.clear();
887 style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}").unwrap();
888 style.format_state(&state, &mut buf, WIDTH);
889 assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
890 }
891
892 #[test]
893 fn align_truncation() {
894 const WIDTH: u16 = 10;
895 let pos = Arc::new(AtomicPosition::new());
896 let mut state = ProgressState::new(Some(10), pos);
897 let mut buf = Vec::new();
898
899 let style = ProgressStyle::with_template("{wide_msg}").unwrap();
900 state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
901 style.format_state(&state, &mut buf, WIDTH);
902 assert_eq!(&buf[0], "abcdefghij");
903
904 buf.clear();
905 let style = ProgressStyle::with_template("{wide_msg:>}").unwrap();
906 state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
907 style.format_state(&state, &mut buf, WIDTH);
908 assert_eq!(&buf[0], "klmnopqrst");
909
910 buf.clear();
911 let style = ProgressStyle::with_template("{wide_msg:^}").unwrap();
912 state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
913 style.format_state(&state, &mut buf, WIDTH);
914 assert_eq!(&buf[0], "fghijklmno");
915 }
916
917 #[test]
918 fn wide_element_style() {
919 const CHARS: &str = "=>-";
920 const WIDTH: u16 = 8;
921 let pos = Arc::new(AtomicPosition::new());
922 // half finished
923 pos.set(2);
924 let mut state = ProgressState::new(Some(4), pos);
925 let mut buf = Vec::new();
926
927 let style = ProgressStyle::with_template("{wide_bar}")
928 .unwrap()
929 .progress_chars(CHARS);
930 style.format_state(&state, &mut buf, WIDTH);
931 assert_eq!(&buf[0], "====>---");
932
933 buf.clear();
934 let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}")
935 .unwrap()
936 .progress_chars(CHARS);
937 style.format_state(&state, &mut buf, WIDTH);
938 assert_eq!(
939 &buf[0],
940 "\u{1b}[31m\u{1b}[44m====>\u{1b}[32m\u{1b}[46m---\u{1b}[0m\u{1b}[0m"
941 );
942
943 buf.clear();
944 let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap();
945 state.message = TabExpandedString::NoTabs("foobar".into());
946 style.format_state(&state, &mut buf, WIDTH);
947 assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m foobar \u{1b}[0m");
948 }
949
950 #[test]
951 fn multiline_handling() {
952 const WIDTH: u16 = 80;
953 let pos = Arc::new(AtomicPosition::new());
954 let mut state = ProgressState::new(Some(10), pos);
955 let mut buf = Vec::new();
956
957 let mut style = ProgressStyle::default_bar();
958 state.message = TabExpandedString::new("foo\nbar\nbaz".into(), 2);
959 style.template = Template::from_str("{msg}").unwrap();
960 style.format_state(&state, &mut buf, WIDTH);
961
962 assert_eq!(buf.len(), 3);
963 assert_eq!(&buf[0], "foo");
964 assert_eq!(&buf[1], "bar");
965 assert_eq!(&buf[2], "baz");
966
967 buf.clear();
968 style.template = Template::from_str("{wide_msg}").unwrap();
969 style.format_state(&state, &mut buf, WIDTH);
970
971 assert_eq!(buf.len(), 3);
972 assert_eq!(&buf[0], "foo");
973 assert_eq!(&buf[1], "bar");
974 assert_eq!(&buf[2], "baz");
975
976 buf.clear();
977 state.prefix = TabExpandedString::new("prefix\nprefix".into(), 2);
978 style.template = Template::from_str("{prefix} {wide_msg}").unwrap();
979 style.format_state(&state, &mut buf, WIDTH);
980
981 assert_eq!(buf.len(), 4);
982 assert_eq!(&buf[0], "prefix");
983 assert_eq!(&buf[1], "prefix foo");
984 assert_eq!(&buf[2], "bar");
985 assert_eq!(&buf[3], "baz");
986 }
987}
988