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