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