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 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 pub fn tick_chars(mut self, s: &str) -> Self {
112 self.tick_strings = s.chars().map(|c| c.to_string().into()).collect();
113 assert!(
116 self.tick_strings.len() >= 2,
117 "at least 2 tick chars required"
118 );
119 self
120 }
121
122 pub fn tick_strings(mut self, s: &[&str]) -> Self {
127 self.tick_strings = s.iter().map(|s| s.to_string().into()).collect();
128 assert!(
131 self.progress_chars.len() >= 2,
132 "at least 2 tick strings required"
133 );
134 self
135 }
136
137 pub fn progress_chars(mut self, s: &str) -> Self {
142 self.progress_chars = segment(s);
143 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 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 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 pub fn get_tick_str(&self, idx: u64) -> &str {
176 &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)]
177 }
178
179 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 let width = width / self.char_width;
187 let fill = fract * width as f32;
189 let entirely_filled = fill as usize;
191 let head = usize::from(fill > 0.0 && entirely_filled < width);
194
195 let cur = if head == 1 {
196 let n = self.progress_chars.len().saturating_sub(2);
198 let cur_char = if n <= 1 {
199 1
202 } else {
203 n.saturating_sub((fill.fract() * n as f32) as usize)
206 };
207 Some(cur_char)
208 } else {
209 None
210 };
211
212 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 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 for (i, line) in expanded.split('\n').enumerate() {
397 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 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
743pub trait ProgressTracker: Send + Sync {
745 fn clone_box(&self) -> Box<dyn ProgressTracker>;
747 fn tick(&mut self, state: &ProgressState, now: Instant);
749 fn reset(&mut self, state: &ProgressState, now: Instant);
751 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 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}