indicatif/
style.rs

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(feature = "unicode-segmentation")]
9use unicode_segmentation::UnicodeSegmentation;
10#[cfg(target_arch = "wasm32")]
11use web_time::Instant;
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| x.to_string().into()).collect()
40}
41
42#[cfg(feature = "unicode-width")]
43fn measure(s: &str) -> usize {
44    unicode_width::UnicodeWidthStr::width(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    c.iter()
56        .map(|s| measure(s.as_ref()))
57        .fold(None, |acc, new| {
58            match acc {
59                None => return Some(new),
60                Some(old) => 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                            "percent_precise" => buf
286                                .write_fmt(format_args!("{:.*}", 3, state.fraction() * 100f32))
287                                .unwrap(),
288                            "bytes" => buf.write_fmt(format_args!("{}", HumanBytes(pos))).unwrap(),
289                            "total_bytes" => {
290                                buf.write_fmt(format_args!("{}", HumanBytes(len))).unwrap();
291                            }
292                            "decimal_bytes" => buf
293                                .write_fmt(format_args!("{}", DecimalBytes(pos)))
294                                .unwrap(),
295                            "decimal_total_bytes" => buf
296                                .write_fmt(format_args!("{}", DecimalBytes(len)))
297                                .unwrap(),
298                            "binary_bytes" => {
299                                buf.write_fmt(format_args!("{}", BinaryBytes(pos))).unwrap();
300                            }
301                            "binary_total_bytes" => {
302                                buf.write_fmt(format_args!("{}", BinaryBytes(len))).unwrap();
303                            }
304                            "elapsed_precise" => buf
305                                .write_fmt(format_args!("{}", FormattedDuration(state.elapsed())))
306                                .unwrap(),
307                            "elapsed" => buf
308                                .write_fmt(format_args!("{:#}", HumanDuration(state.elapsed())))
309                                .unwrap(),
310                            "per_sec" => buf
311                                .write_fmt(format_args!("{}/s", HumanFloatCount(state.per_sec())))
312                                .unwrap(),
313                            "bytes_per_sec" => buf
314                                .write_fmt(format_args!("{}/s", HumanBytes(state.per_sec() as u64)))
315                                .unwrap(),
316                            "decimal_bytes_per_sec" => buf
317                                .write_fmt(format_args!(
318                                    "{}/s",
319                                    DecimalBytes(state.per_sec() as u64)
320                                ))
321                                .unwrap(),
322                            "binary_bytes_per_sec" => buf
323                                .write_fmt(format_args!(
324                                    "{}/s",
325                                    BinaryBytes(state.per_sec() as u64)
326                                ))
327                                .unwrap(),
328                            "eta_precise" => buf
329                                .write_fmt(format_args!("{}", FormattedDuration(state.eta())))
330                                .unwrap(),
331                            "eta" => buf
332                                .write_fmt(format_args!("{:#}", HumanDuration(state.eta())))
333                                .unwrap(),
334                            "duration_precise" => buf
335                                .write_fmt(format_args!("{}", FormattedDuration(state.duration())))
336                                .unwrap(),
337                            "duration" => buf
338                                .write_fmt(format_args!("{:#}", HumanDuration(state.duration())))
339                                .unwrap(),
340                            _ => (),
341                        }
342                    };
343
344                    match width {
345                        Some(width) => {
346                            let padded = PaddedStringDisplay {
347                                str: &buf,
348                                width: *width as usize,
349                                align: *align,
350                                truncate: *truncate,
351                            };
352                            match style {
353                                Some(s) => cur
354                                    .write_fmt(format_args!("{}", s.apply_to(padded)))
355                                    .unwrap(),
356                                None => cur.write_fmt(format_args!("{padded}")).unwrap(),
357                            }
358                        }
359                        None => match style {
360                            Some(s) => cur.write_fmt(format_args!("{}", s.apply_to(&buf))).unwrap(),
361                            None => cur.push_str(&buf),
362                        },
363                    }
364                }
365                TemplatePart::Literal(s) => cur.push_str(s.expanded()),
366                TemplatePart::NewLine => {
367                    self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
368                }
369            }
370        }
371
372        if !cur.is_empty() {
373            self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
374        }
375    }
376
377    fn push_line(
378        &self,
379        lines: &mut Vec<String>,
380        cur: &mut String,
381        state: &ProgressState,
382        buf: &mut String,
383        target_width: u16,
384        wide: &Option<WideElement>,
385    ) {
386        let expanded = match wide {
387            Some(inner) => inner.expand(mem::take(cur), self, state, buf, target_width),
388            None => mem::take(cur),
389        };
390
391        // If there are newlines, we need to split them up
392        // and add the lines separately so that they're counted
393        // correctly on re-render.
394        for (i, line) in expanded.split('\n').enumerate() {
395            // No newlines found in this case
396            if i == 0 && line.len() == expanded.len() {
397                lines.push(expanded);
398                break;
399            }
400
401            lines.push(line.to_string());
402        }
403    }
404}
405
406struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize);
407
408impl Write for TabRewriter<'_> {
409    fn write_str(&mut self, s: &str) -> fmt::Result {
410        self.0
411            .write_str(s.replace('\t', &" ".repeat(self.1)).as_str())
412    }
413}
414
415#[derive(Clone, Copy)]
416enum WideElement<'a> {
417    Bar { alt_style: &'a Option<Style> },
418    Message { align: &'a Alignment },
419}
420
421impl<'a> WideElement<'a> {
422    fn expand(
423        self,
424        cur: String,
425        style: &ProgressStyle,
426        state: &ProgressState,
427        buf: &mut String,
428        width: u16,
429    ) -> String {
430        let left = (width as usize).saturating_sub(measure_text_width(&cur.replace('\x00', "")));
431        match self {
432            Self::Bar { alt_style } => cur.replace(
433                '\x00',
434                &format!(
435                    "{}",
436                    style.format_bar(state.fraction(), left, alt_style.as_ref())
437                ),
438            ),
439            WideElement::Message { align } => {
440                buf.clear();
441                buf.write_fmt(format_args!(
442                    "{}",
443                    PaddedStringDisplay {
444                        str: state.message.expanded(),
445                        width: left,
446                        align: *align,
447                        truncate: true,
448                    }
449                ))
450                .unwrap();
451
452                let trimmed = match cur.as_bytes().last() == Some(&b'\x00') {
453                    true => buf.trim_end(),
454                    false => buf,
455                };
456
457                cur.replace('\x00', trimmed)
458            }
459        }
460    }
461}
462
463#[derive(Clone, Debug)]
464struct Template {
465    parts: Vec<TemplatePart>,
466}
467
468impl Template {
469    fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> {
470        use State::*;
471        let (mut state, mut parts, mut buf) = (Literal, vec![], String::new());
472        for c in s.chars() {
473            let new = match (state, c) {
474                (Literal, '{') => (MaybeOpen, None),
475                (Literal, '\n') => {
476                    if !buf.is_empty() {
477                        parts.push(TemplatePart::Literal(TabExpandedString::new(
478                            mem::take(&mut buf).into(),
479                            tab_width,
480                        )));
481                    }
482                    parts.push(TemplatePart::NewLine);
483                    (Literal, None)
484                }
485                (Literal, '}') => (DoubleClose, Some('}')),
486                (Literal, c) => (Literal, Some(c)),
487                (DoubleClose, '}') => (Literal, None),
488                (MaybeOpen, '{') => (Literal, Some('{')),
489                (MaybeOpen | Key, c) if c.is_ascii_whitespace() => {
490                    // If we find whitespace where the variable key is supposed to go,
491                    // backtrack and act as if this was a literal.
492                    buf.push(c);
493                    let mut new = String::from("{");
494                    new.push_str(&buf);
495                    buf.clear();
496                    parts.push(TemplatePart::Literal(TabExpandedString::new(
497                        new.into(),
498                        tab_width,
499                    )));
500                    (Literal, None)
501                }
502                (MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)),
503                (Key, c) if c != '}' && c != ':' => (Key, Some(c)),
504                (Key, ':') => (Align, None),
505                (Key, '}') => (Literal, None),
506                (Key, '!') if !buf.is_empty() => {
507                    parts.push(TemplatePart::Placeholder {
508                        key: mem::take(&mut buf),
509                        align: Alignment::Left,
510                        width: None,
511                        truncate: true,
512                        style: None,
513                        alt_style: None,
514                    });
515                    (Width, None)
516                }
517                (Align, c) if c == '<' || c == '^' || c == '>' => {
518                    if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() {
519                        match c {
520                            '<' => *align = Alignment::Left,
521                            '^' => *align = Alignment::Center,
522                            '>' => *align = Alignment::Right,
523                            _ => (),
524                        }
525                    }
526
527                    (Width, None)
528                }
529                (Align, c @ '0'..='9') => (Width, Some(c)),
530                (Align | Width, '!') => {
531                    if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() {
532                        *truncate = true;
533                    }
534                    (Width, None)
535                }
536                (Align, '.') => (FirstStyle, None),
537                (Align, '}') => (Literal, None),
538                (Width, c @ '0'..='9') => (Width, Some(c)),
539                (Width, '.') => (FirstStyle, None),
540                (Width, '}') => (Literal, None),
541                (FirstStyle, '/') => (AltStyle, None),
542                (FirstStyle, '}') => (Literal, None),
543                (FirstStyle, c) => (FirstStyle, Some(c)),
544                (AltStyle, '}') => (Literal, None),
545                (AltStyle, c) => (AltStyle, Some(c)),
546                (st, c) => return Err(TemplateError { next: c, state: st }),
547            };
548
549            match (state, new.0) {
550                (MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal(
551                    TabExpandedString::new(mem::take(&mut buf).into(), tab_width),
552                )),
553                (Key, Align | Literal) if !buf.is_empty() => {
554                    parts.push(TemplatePart::Placeholder {
555                        key: mem::take(&mut buf),
556                        align: Alignment::Left,
557                        width: None,
558                        truncate: false,
559                        style: None,
560                        alt_style: None,
561                    });
562                }
563                (Width, FirstStyle | Literal) if !buf.is_empty() => {
564                    if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() {
565                        *width = Some(buf.parse().unwrap());
566                        buf.clear();
567                    }
568                }
569                (FirstStyle, AltStyle | Literal) if !buf.is_empty() => {
570                    if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() {
571                        *style = Some(Style::from_dotted_str(&buf));
572                        buf.clear();
573                    }
574                }
575                (AltStyle, Literal) if !buf.is_empty() => {
576                    if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() {
577                        *alt_style = Some(Style::from_dotted_str(&buf));
578                        buf.clear();
579                    }
580                }
581                (_, _) => (),
582            }
583
584            state = new.0;
585            if let Some(c) = new.1 {
586                buf.push(c);
587            }
588        }
589
590        if matches!(state, Literal | DoubleClose) && !buf.is_empty() {
591            parts.push(TemplatePart::Literal(TabExpandedString::new(
592                buf.into(),
593                tab_width,
594            )));
595        }
596
597        Ok(Self { parts })
598    }
599
600    fn from_str(s: &str) -> Result<Self, TemplateError> {
601        Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH)
602    }
603
604    fn set_tab_width(&mut self, new_tab_width: usize) {
605        for part in &mut self.parts {
606            if let TemplatePart::Literal(s) = part {
607                s.set_tab_width(new_tab_width);
608            }
609        }
610    }
611}
612
613#[derive(Debug)]
614pub struct TemplateError {
615    state: State,
616    next: char,
617}
618
619impl fmt::Display for TemplateError {
620    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
621        write!(
622            f,
623            "TemplateError: unexpected character {:?} in state {:?}",
624            self.next, self.state
625        )
626    }
627}
628
629impl std::error::Error for TemplateError {}
630
631#[derive(Clone, Debug, PartialEq, Eq)]
632enum TemplatePart {
633    Literal(TabExpandedString),
634    Placeholder {
635        key: String,
636        align: Alignment,
637        width: Option<u16>,
638        truncate: bool,
639        style: Option<Style>,
640        alt_style: Option<Style>,
641    },
642    NewLine,
643}
644
645#[derive(Copy, Clone, Debug, PartialEq, Eq)]
646enum State {
647    Literal,
648    MaybeOpen,
649    DoubleClose,
650    Key,
651    Align,
652    Width,
653    FirstStyle,
654    AltStyle,
655}
656
657struct BarDisplay<'a> {
658    chars: &'a [Box<str>],
659    filled: usize,
660    cur: Option<usize>,
661    rest: console::StyledObject<RepeatedStringDisplay<'a>>,
662}
663
664impl<'a> fmt::Display for BarDisplay<'a> {
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        for _ in 0..self.filled {
667            f.write_str(&self.chars[0])?;
668        }
669        if let Some(cur) = self.cur {
670            f.write_str(&self.chars[cur])?;
671        }
672        self.rest.fmt(f)
673    }
674}
675
676struct RepeatedStringDisplay<'a> {
677    str: &'a str,
678    num: usize,
679}
680
681impl<'a> fmt::Display for RepeatedStringDisplay<'a> {
682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
683        for _ in 0..self.num {
684            f.write_str(self.str)?;
685        }
686        Ok(())
687    }
688}
689
690struct PaddedStringDisplay<'a> {
691    str: &'a str,
692    width: usize,
693    align: Alignment,
694    truncate: bool,
695}
696
697impl<'a> fmt::Display for PaddedStringDisplay<'a> {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        let cols = measure_text_width(self.str);
700        let excess = cols.saturating_sub(self.width);
701        if excess > 0 && !self.truncate {
702            return f.write_str(self.str);
703        } else if excess > 0 {
704            let (start, end) = match self.align {
705                Alignment::Left => (0, self.str.len() - excess),
706                Alignment::Right => (excess, self.str.len()),
707                Alignment::Center => (
708                    excess / 2,
709                    self.str.len() - excess.saturating_sub(excess / 2),
710                ),
711            };
712
713            return f.write_str(self.str.get(start..end).unwrap_or(self.str));
714        }
715
716        let diff = self.width.saturating_sub(cols);
717        let (left_pad, right_pad) = match self.align {
718            Alignment::Left => (0, diff),
719            Alignment::Right => (diff, 0),
720            Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)),
721        };
722
723        for _ in 0..left_pad {
724            f.write_char(' ')?;
725        }
726        f.write_str(self.str)?;
727        for _ in 0..right_pad {
728            f.write_char(' ')?;
729        }
730        Ok(())
731    }
732}
733
734#[derive(PartialEq, Eq, Debug, Copy, Clone)]
735enum Alignment {
736    Left,
737    Center,
738    Right,
739}
740
741/// Trait for defining stateful or stateless formatters
742pub trait ProgressTracker: Send + Sync {
743    /// Creates a new instance of the progress tracker
744    fn clone_box(&self) -> Box<dyn ProgressTracker>;
745    /// Notifies the progress tracker of a tick event
746    fn tick(&mut self, state: &ProgressState, now: Instant);
747    /// Notifies the progress tracker of a reset event
748    fn reset(&mut self, state: &ProgressState, now: Instant);
749    /// Provides access to the progress bar display buffer for custom messages
750    fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write);
751}
752
753impl Clone for Box<dyn ProgressTracker> {
754    fn clone(&self) -> Self {
755        self.clone_box()
756    }
757}
758
759impl<F> ProgressTracker for F
760where
761    F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Sync + Clone + 'static,
762{
763    fn clone_box(&self) -> Box<dyn ProgressTracker> {
764        Box::new(self.clone())
765    }
766
767    fn tick(&mut self, _: &ProgressState, _: Instant) {}
768
769    fn reset(&mut self, _: &ProgressState, _: Instant) {}
770
771    fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) {
772        (self)(state, w);
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use std::sync::Arc;
779
780    use super::*;
781    use crate::state::{AtomicPosition, ProgressState};
782
783    use console::set_colors_enabled;
784    use std::sync::Mutex;
785
786    #[test]
787    fn test_stateful_tracker() {
788        #[derive(Debug, Clone)]
789        struct TestTracker(Arc<Mutex<String>>);
790
791        impl ProgressTracker for TestTracker {
792            fn clone_box(&self) -> Box<dyn ProgressTracker> {
793                Box::new(self.clone())
794            }
795
796            fn tick(&mut self, state: &ProgressState, _: Instant) {
797                let mut m = self.0.lock().unwrap();
798                m.clear();
799                m.push_str(format!("{} {}", state.len().unwrap(), state.pos()).as_str());
800            }
801
802            fn reset(&mut self, _state: &ProgressState, _: Instant) {
803                let mut m = self.0.lock().unwrap();
804                m.clear();
805            }
806
807            fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) {
808                w.write_str(self.0.lock().unwrap().as_str()).unwrap();
809            }
810        }
811
812        use crate::ProgressBar;
813
814        let pb = ProgressBar::new(1);
815        pb.set_style(
816            ProgressStyle::with_template("{{ {foo} }}")
817                .unwrap()
818                .with_key("foo", TestTracker(Arc::new(Mutex::new(String::default()))))
819                .progress_chars("#>-"),
820        );
821
822        let mut buf = Vec::new();
823        let style = pb.clone().style();
824
825        style.format_state(&pb.state().state, &mut buf, 16);
826        assert_eq!(&buf[0], "{  }");
827        buf.clear();
828        pb.inc(1);
829        style.format_state(&pb.state().state, &mut buf, 16);
830        assert_eq!(&buf[0], "{ 1 1 }");
831        pb.reset();
832        buf.clear();
833        style.format_state(&pb.state().state, &mut buf, 16);
834        assert_eq!(&buf[0], "{  }");
835        pb.finish_and_clear();
836    }
837
838    use crate::state::TabExpandedString;
839
840    #[test]
841    fn test_expand_template() {
842        const WIDTH: u16 = 80;
843        let pos = Arc::new(AtomicPosition::new());
844        let state = ProgressState::new(Some(10), pos);
845        let mut buf = Vec::new();
846
847        let mut style = ProgressStyle::default_bar();
848        style.format_map.insert(
849            "foo",
850            Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO").unwrap()),
851        );
852        style.format_map.insert(
853            "bar",
854            Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR").unwrap()),
855        );
856
857        style.template = Template::from_str("{{ {foo} {bar} }}").unwrap();
858        style.format_state(&state, &mut buf, WIDTH);
859        assert_eq!(&buf[0], "{ FOO BAR }");
860
861        buf.clear();
862        style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"#).unwrap();
863        style.format_state(&state, &mut buf, WIDTH);
864        assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"#);
865    }
866
867    #[test]
868    fn test_expand_template_flags() {
869        set_colors_enabled(true);
870
871        const WIDTH: u16 = 80;
872        let pos = Arc::new(AtomicPosition::new());
873        let state = ProgressState::new(Some(10), pos);
874        let mut buf = Vec::new();
875
876        let mut style = ProgressStyle::default_bar();
877        style.format_map.insert(
878            "foo",
879            Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX").unwrap()),
880        );
881
882        style.template = Template::from_str("{foo:5}").unwrap();
883        style.format_state(&state, &mut buf, WIDTH);
884        assert_eq!(&buf[0], "XXX  ");
885
886        buf.clear();
887        style.template = Template::from_str("{foo:.red.on_blue}").unwrap();
888        style.format_state(&state, &mut buf, WIDTH);
889        assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m");
890
891        buf.clear();
892        style.template = Template::from_str("{foo:^5.red.on_blue}").unwrap();
893        style.format_state(&state, &mut buf, WIDTH);
894        assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
895
896        buf.clear();
897        style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}").unwrap();
898        style.format_state(&state, &mut buf, WIDTH);
899        assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
900    }
901
902    #[test]
903    fn align_truncation() {
904        const WIDTH: u16 = 10;
905        let pos = Arc::new(AtomicPosition::new());
906        let mut state = ProgressState::new(Some(10), pos);
907        let mut buf = Vec::new();
908
909        let style = ProgressStyle::with_template("{wide_msg}").unwrap();
910        state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
911        style.format_state(&state, &mut buf, WIDTH);
912        assert_eq!(&buf[0], "abcdefghij");
913
914        buf.clear();
915        let style = ProgressStyle::with_template("{wide_msg:>}").unwrap();
916        state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
917        style.format_state(&state, &mut buf, WIDTH);
918        assert_eq!(&buf[0], "klmnopqrst");
919
920        buf.clear();
921        let style = ProgressStyle::with_template("{wide_msg:^}").unwrap();
922        state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
923        style.format_state(&state, &mut buf, WIDTH);
924        assert_eq!(&buf[0], "fghijklmno");
925    }
926
927    #[test]
928    fn wide_element_style() {
929        set_colors_enabled(true);
930
931        const CHARS: &str = "=>-";
932        const WIDTH: u16 = 8;
933        let pos = Arc::new(AtomicPosition::new());
934        // half finished
935        pos.set(2);
936        let mut state = ProgressState::new(Some(4), pos);
937        let mut buf = Vec::new();
938
939        let style = ProgressStyle::with_template("{wide_bar}")
940            .unwrap()
941            .progress_chars(CHARS);
942        style.format_state(&state, &mut buf, WIDTH);
943        assert_eq!(&buf[0], "====>---");
944
945        buf.clear();
946        let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}")
947            .unwrap()
948            .progress_chars(CHARS);
949        style.format_state(&state, &mut buf, WIDTH);
950        assert_eq!(
951            &buf[0],
952            "\u{1b}[31m\u{1b}[44m====>\u{1b}[32m\u{1b}[46m---\u{1b}[0m\u{1b}[0m"
953        );
954
955        buf.clear();
956        let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap();
957        state.message = TabExpandedString::NoTabs("foobar".into());
958        style.format_state(&state, &mut buf, WIDTH);
959        assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m foobar \u{1b}[0m");
960    }
961
962    #[test]
963    fn multiline_handling() {
964        const WIDTH: u16 = 80;
965        let pos = Arc::new(AtomicPosition::new());
966        let mut state = ProgressState::new(Some(10), pos);
967        let mut buf = Vec::new();
968
969        let mut style = ProgressStyle::default_bar();
970        state.message = TabExpandedString::new("foo\nbar\nbaz".into(), 2);
971        style.template = Template::from_str("{msg}").unwrap();
972        style.format_state(&state, &mut buf, WIDTH);
973
974        assert_eq!(buf.len(), 3);
975        assert_eq!(&buf[0], "foo");
976        assert_eq!(&buf[1], "bar");
977        assert_eq!(&buf[2], "baz");
978
979        buf.clear();
980        style.template = Template::from_str("{wide_msg}").unwrap();
981        style.format_state(&state, &mut buf, WIDTH);
982
983        assert_eq!(buf.len(), 3);
984        assert_eq!(&buf[0], "foo");
985        assert_eq!(&buf[1], "bar");
986        assert_eq!(&buf[2], "baz");
987
988        buf.clear();
989        state.prefix = TabExpandedString::new("prefix\nprefix".into(), 2);
990        style.template = Template::from_str("{prefix} {wide_msg}").unwrap();
991        style.format_state(&state, &mut buf, WIDTH);
992
993        assert_eq!(buf.len(), 4);
994        assert_eq!(&buf[0], "prefix");
995        assert_eq!(&buf[1], "prefix foo");
996        assert_eq!(&buf[2], "bar");
997        assert_eq!(&buf[3], "baz");
998    }
999}