indicatif/
style.rs

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