indicatif/
style.rs

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