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