Skip to main content

indicatif/
style.rs

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