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