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