summaryrefslogtreecommitdiff
path: root/vendor/indicatif/src/style.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/indicatif/src/style.rs')
-rw-r--r--vendor/indicatif/src/style.rs987
1 files changed, 987 insertions, 0 deletions
diff --git a/vendor/indicatif/src/style.rs b/vendor/indicatif/src/style.rs
new file mode 100644
index 0000000..01b220f
--- /dev/null
+++ b/vendor/indicatif/src/style.rs
@@ -0,0 +1,987 @@
+use std::collections::HashMap;
+use std::fmt::{self, Write};
+use std::mem;
+#[cfg(not(target_arch = "wasm32"))]
+use std::time::Instant;
+
+use console::{measure_text_width, Style};
+#[cfg(target_arch = "wasm32")]
+use instant::Instant;
+#[cfg(feature = "unicode-segmentation")]
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::format::{
+ BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
+ HumanFloatCount,
+};
+use crate::state::{ProgressState, TabExpandedString, DEFAULT_TAB_WIDTH};
+
+#[derive(Clone)]
+pub struct ProgressStyle {
+ tick_strings: Vec<Box<str>>,
+ progress_chars: Vec<Box<str>>,
+ template: Template,
+ // how unicode-big each char in progress_chars is
+ char_width: usize,
+ tab_width: usize,
+ pub(crate) format_map: HashMap<&'static str, Box<dyn ProgressTracker>>,
+}
+
+#[cfg(feature = "unicode-segmentation")]
+fn segment(s: &str) -> Vec<Box<str>> {
+ UnicodeSegmentation::graphemes(s, true)
+ .map(|s| s.into())
+ .collect()
+}
+
+#[cfg(not(feature = "unicode-segmentation"))]
+fn segment(s: &str) -> Vec<Box<str>> {
+ s.chars().map(|x| x.to_string().into()).collect()
+}
+
+#[cfg(feature = "unicode-width")]
+fn measure(s: &str) -> usize {
+ unicode_width::UnicodeWidthStr::width(s)
+}
+
+#[cfg(not(feature = "unicode-width"))]
+fn measure(s: &str) -> usize {
+ s.chars().count()
+}
+
+/// finds the unicode-aware width of the passed grapheme cluters
+/// panics on an empty parameter, or if the characters are not equal-width
+fn width(c: &[Box<str>]) -> usize {
+ c.iter()
+ .map(|s| measure(s.as_ref()))
+ .fold(None, |acc, new| {
+ match acc {
+ None => return Some(new),
+ Some(old) => assert_eq!(old, new, "got passed un-equal width progress characters"),
+ }
+ acc
+ })
+ .unwrap()
+}
+
+impl ProgressStyle {
+ /// Returns the default progress bar style for bars
+ pub fn default_bar() -> Self {
+ Self::new(Template::from_str("{wide_bar} {pos}/{len}").unwrap())
+ }
+
+ /// Returns the default progress bar style for spinners
+ pub fn default_spinner() -> Self {
+ Self::new(Template::from_str("{spinner} {msg}").unwrap())
+ }
+
+ /// Sets the template string for the progress bar
+ ///
+ /// Review the [list of template keys](../index.html#templates) for more information.
+ pub fn with_template(template: &str) -> Result<Self, TemplateError> {
+ Ok(Self::new(Template::from_str(template)?))
+ }
+
+ pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) {
+ self.tab_width = new_tab_width;
+ self.template.set_tab_width(new_tab_width);
+ }
+
+ fn new(template: Template) -> Self {
+ let progress_chars = segment("█░");
+ let char_width = width(&progress_chars);
+ Self {
+ tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ "
+ .chars()
+ .map(|c| c.to_string().into())
+ .collect(),
+ progress_chars,
+ char_width,
+ template,
+ format_map: HashMap::default(),
+ tab_width: DEFAULT_TAB_WIDTH,
+ }
+ }
+
+ /// Sets the tick character sequence for spinners
+ ///
+ /// Note that the last character is used as the [final tick string][Self::get_final_tick_str()].
+ /// At least two characters are required to provide a non-final and final state.
+ pub fn tick_chars(mut self, s: &str) -> Self {
+ self.tick_strings = s.chars().map(|c| c.to_string().into()).collect();
+ // Format bar will panic with some potentially confusing message, better to panic here
+ // with a message explicitly informing of the problem
+ assert!(
+ self.tick_strings.len() >= 2,
+ "at least 2 tick chars required"
+ );
+ self
+ }
+
+ /// Sets the tick string sequence for spinners
+ ///
+ /// Note that the last string is used as the [final tick string][Self::get_final_tick_str()].
+ /// At least two strings are required to provide a non-final and final state.
+ pub fn tick_strings(mut self, s: &[&str]) -> Self {
+ self.tick_strings = s.iter().map(|s| s.to_string().into()).collect();
+ // Format bar will panic with some potentially confusing message, better to panic here
+ // with a message explicitly informing of the problem
+ assert!(
+ self.progress_chars.len() >= 2,
+ "at least 2 tick strings required"
+ );
+ self
+ }
+
+ /// Sets the progress characters `(filled, current, to do)`
+ ///
+ /// You can pass more than three for a more detailed display.
+ /// All passed grapheme clusters need to be of equal width.
+ pub fn progress_chars(mut self, s: &str) -> Self {
+ self.progress_chars = segment(s);
+ // Format bar will panic with some potentially confusing message, better to panic here
+ // with a message explicitly informing of the problem
+ assert!(
+ self.progress_chars.len() >= 2,
+ "at least 2 progress chars required"
+ );
+ self.char_width = width(&self.progress_chars);
+ self
+ }
+
+ /// Adds a custom key that owns a [`ProgressTracker`] to the template
+ pub fn with_key<S: ProgressTracker + 'static>(mut self, key: &'static str, f: S) -> Self {
+ self.format_map.insert(key, Box::new(f));
+ self
+ }
+
+ /// Sets the template string for the progress bar
+ ///
+ /// Review the [list of template keys](../index.html#templates) for more information.
+ pub fn template(mut self, s: &str) -> Result<Self, TemplateError> {
+ self.template = Template::from_str(s)?;
+ Ok(self)
+ }
+
+ fn current_tick_str(&self, state: &ProgressState) -> &str {
+ match state.is_finished() {
+ true => self.get_final_tick_str(),
+ false => self.get_tick_str(state.tick),
+ }
+ }
+
+ /// Returns the tick string for a given number
+ pub fn get_tick_str(&self, idx: u64) -> &str {
+ &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)]
+ }
+
+ /// Returns the tick string for the finished state
+ pub fn get_final_tick_str(&self) -> &str {
+ &self.tick_strings[self.tick_strings.len() - 1]
+ }
+
+ fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> BarDisplay<'_> {
+ // The number of clusters from progress_chars to write (rounding down).
+ let width = width / self.char_width;
+ // The number of full clusters (including a fractional component for a partially-full one).
+ let fill = fract * width as f32;
+ // The number of entirely full clusters (by truncating `fill`).
+ let entirely_filled = fill as usize;
+ // 1 if the bar is not entirely empty or full (meaning we need to draw the "current"
+ // character between the filled and "to do" segment), 0 otherwise.
+ let head = usize::from(fill > 0.0 && entirely_filled < width);
+
+ let cur = if head == 1 {
+ // Number of fine-grained progress entries in progress_chars.
+ let n = self.progress_chars.len().saturating_sub(2);
+ let cur_char = if n <= 1 {
+ // No fine-grained entries. 1 is the single "current" entry if we have one, the "to
+ // do" entry if not.
+ 1
+ } else {
+ // Pick a fine-grained entry, ranging from the last one (n) if the fractional part
+ // of fill is 0 to the first one (1) if the fractional part of fill is almost 1.
+ n.saturating_sub((fill.fract() * n as f32) as usize)
+ };
+ Some(cur_char)
+ } else {
+ None
+ };
+
+ // Number of entirely empty clusters needed to fill the bar up to `width`.
+ let bg = width.saturating_sub(entirely_filled).saturating_sub(head);
+ let rest = RepeatedStringDisplay {
+ str: &self.progress_chars[self.progress_chars.len() - 1],
+ num: bg,
+ };
+
+ BarDisplay {
+ chars: &self.progress_chars,
+ filled: entirely_filled,
+ cur,
+ rest: alt_style.unwrap_or(&Style::new()).apply_to(rest),
+ }
+ }
+
+ pub(crate) fn format_state(
+ &self,
+ state: &ProgressState,
+ lines: &mut Vec<String>,
+ target_width: u16,
+ ) {
+ let mut cur = String::new();
+ let mut buf = String::new();
+ let mut wide = None;
+
+ let pos = state.pos();
+ let len = state.len().unwrap_or(pos);
+ for part in &self.template.parts {
+ match part {
+ TemplatePart::Placeholder {
+ key,
+ align,
+ width,
+ truncate,
+ style,
+ alt_style,
+ } => {
+ buf.clear();
+ if let Some(tracker) = self.format_map.get(key.as_str()) {
+ tracker.write(state, &mut TabRewriter(&mut buf, self.tab_width));
+ } else {
+ match key.as_str() {
+ "wide_bar" => {
+ wide = Some(WideElement::Bar { alt_style });
+ buf.push('\x00');
+ }
+ "bar" => buf
+ .write_fmt(format_args!(
+ "{}",
+ self.format_bar(
+ state.fraction(),
+ width.unwrap_or(20) as usize,
+ alt_style.as_ref(),
+ )
+ ))
+ .unwrap(),
+ "spinner" => buf.push_str(self.current_tick_str(state)),
+ "wide_msg" => {
+ wide = Some(WideElement::Message { align });
+ buf.push('\x00');
+ }
+ "msg" => buf.push_str(state.message.expanded()),
+ "prefix" => buf.push_str(state.prefix.expanded()),
+ "pos" => buf.write_fmt(format_args!("{pos}")).unwrap(),
+ "human_pos" => {
+ buf.write_fmt(format_args!("{}", HumanCount(pos))).unwrap();
+ }
+ "len" => buf.write_fmt(format_args!("{len}")).unwrap(),
+ "human_len" => {
+ buf.write_fmt(format_args!("{}", HumanCount(len))).unwrap();
+ }
+ "percent" => buf
+ .write_fmt(format_args!("{:.*}", 0, state.fraction() * 100f32))
+ .unwrap(),
+ "bytes" => buf.write_fmt(format_args!("{}", HumanBytes(pos))).unwrap(),
+ "total_bytes" => {
+ buf.write_fmt(format_args!("{}", HumanBytes(len))).unwrap();
+ }
+ "decimal_bytes" => buf
+ .write_fmt(format_args!("{}", DecimalBytes(pos)))
+ .unwrap(),
+ "decimal_total_bytes" => buf
+ .write_fmt(format_args!("{}", DecimalBytes(len)))
+ .unwrap(),
+ "binary_bytes" => {
+ buf.write_fmt(format_args!("{}", BinaryBytes(pos))).unwrap();
+ }
+ "binary_total_bytes" => {
+ buf.write_fmt(format_args!("{}", BinaryBytes(len))).unwrap();
+ }
+ "elapsed_precise" => buf
+ .write_fmt(format_args!("{}", FormattedDuration(state.elapsed())))
+ .unwrap(),
+ "elapsed" => buf
+ .write_fmt(format_args!("{:#}", HumanDuration(state.elapsed())))
+ .unwrap(),
+ "per_sec" => buf
+ .write_fmt(format_args!("{}/s", HumanFloatCount(state.per_sec())))
+ .unwrap(),
+ "bytes_per_sec" => buf
+ .write_fmt(format_args!("{}/s", HumanBytes(state.per_sec() as u64)))
+ .unwrap(),
+ "binary_bytes_per_sec" => buf
+ .write_fmt(format_args!(
+ "{}/s",
+ BinaryBytes(state.per_sec() as u64)
+ ))
+ .unwrap(),
+ "eta_precise" => buf
+ .write_fmt(format_args!("{}", FormattedDuration(state.eta())))
+ .unwrap(),
+ "eta" => buf
+ .write_fmt(format_args!("{:#}", HumanDuration(state.eta())))
+ .unwrap(),
+ "duration_precise" => buf
+ .write_fmt(format_args!("{}", FormattedDuration(state.duration())))
+ .unwrap(),
+ "duration" => buf
+ .write_fmt(format_args!("{:#}", HumanDuration(state.duration())))
+ .unwrap(),
+ _ => (),
+ }
+ };
+
+ match width {
+ Some(width) => {
+ let padded = PaddedStringDisplay {
+ str: &buf,
+ width: *width as usize,
+ align: *align,
+ truncate: *truncate,
+ };
+ match style {
+ Some(s) => cur
+ .write_fmt(format_args!("{}", s.apply_to(padded)))
+ .unwrap(),
+ None => cur.write_fmt(format_args!("{padded}")).unwrap(),
+ }
+ }
+ None => match style {
+ Some(s) => cur.write_fmt(format_args!("{}", s.apply_to(&buf))).unwrap(),
+ None => cur.push_str(&buf),
+ },
+ }
+ }
+ TemplatePart::Literal(s) => cur.push_str(s.expanded()),
+ TemplatePart::NewLine => {
+ self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
+ }
+ }
+ }
+
+ if !cur.is_empty() {
+ self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide);
+ }
+ }
+
+ fn push_line(
+ &self,
+ lines: &mut Vec<String>,
+ cur: &mut String,
+ state: &ProgressState,
+ buf: &mut String,
+ target_width: u16,
+ wide: &Option<WideElement>,
+ ) {
+ let expanded = match wide {
+ Some(inner) => inner.expand(mem::take(cur), self, state, buf, target_width),
+ None => mem::take(cur),
+ };
+
+ // If there are newlines, we need to split them up
+ // and add the lines separately so that they're counted
+ // correctly on re-render.
+ for (i, line) in expanded.split('\n').enumerate() {
+ // No newlines found in this case
+ if i == 0 && line.len() == expanded.len() {
+ lines.push(expanded);
+ break;
+ }
+
+ lines.push(line.to_string());
+ }
+ }
+}
+
+struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize);
+
+impl Write for TabRewriter<'_> {
+ fn write_str(&mut self, s: &str) -> fmt::Result {
+ self.0
+ .write_str(s.replace('\t', &" ".repeat(self.1)).as_str())
+ }
+}
+
+#[derive(Clone, Copy)]
+enum WideElement<'a> {
+ Bar { alt_style: &'a Option<Style> },
+ Message { align: &'a Alignment },
+}
+
+impl<'a> WideElement<'a> {
+ fn expand(
+ self,
+ cur: String,
+ style: &ProgressStyle,
+ state: &ProgressState,
+ buf: &mut String,
+ width: u16,
+ ) -> String {
+ let left = (width as usize).saturating_sub(measure_text_width(&cur.replace('\x00', "")));
+ match self {
+ Self::Bar { alt_style } => cur.replace(
+ '\x00',
+ &format!(
+ "{}",
+ style.format_bar(state.fraction(), left, alt_style.as_ref())
+ ),
+ ),
+ WideElement::Message { align } => {
+ buf.clear();
+ buf.write_fmt(format_args!(
+ "{}",
+ PaddedStringDisplay {
+ str: state.message.expanded(),
+ width: left,
+ align: *align,
+ truncate: true,
+ }
+ ))
+ .unwrap();
+
+ let trimmed = match cur.as_bytes().last() == Some(&b'\x00') {
+ true => buf.trim_end(),
+ false => buf,
+ };
+
+ cur.replace('\x00', trimmed)
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+struct Template {
+ parts: Vec<TemplatePart>,
+}
+
+impl Template {
+ fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> {
+ use State::*;
+ let (mut state, mut parts, mut buf) = (Literal, vec![], String::new());
+ for c in s.chars() {
+ let new = match (state, c) {
+ (Literal, '{') => (MaybeOpen, None),
+ (Literal, '\n') => {
+ if !buf.is_empty() {
+ parts.push(TemplatePart::Literal(TabExpandedString::new(
+ mem::take(&mut buf).into(),
+ tab_width,
+ )));
+ }
+ parts.push(TemplatePart::NewLine);
+ (Literal, None)
+ }
+ (Literal, '}') => (DoubleClose, Some('}')),
+ (Literal, c) => (Literal, Some(c)),
+ (DoubleClose, '}') => (Literal, None),
+ (MaybeOpen, '{') => (Literal, Some('{')),
+ (MaybeOpen | Key, c) if c.is_ascii_whitespace() => {
+ // If we find whitespace where the variable key is supposed to go,
+ // backtrack and act as if this was a literal.
+ buf.push(c);
+ let mut new = String::from("{");
+ new.push_str(&buf);
+ buf.clear();
+ parts.push(TemplatePart::Literal(TabExpandedString::new(
+ new.into(),
+ tab_width,
+ )));
+ (Literal, None)
+ }
+ (MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)),
+ (Key, c) if c != '}' && c != ':' => (Key, Some(c)),
+ (Key, ':') => (Align, None),
+ (Key, '}') => (Literal, None),
+ (Key, '!') if !buf.is_empty() => {
+ parts.push(TemplatePart::Placeholder {
+ key: mem::take(&mut buf),
+ align: Alignment::Left,
+ width: None,
+ truncate: true,
+ style: None,
+ alt_style: None,
+ });
+ (Width, None)
+ }
+ (Align, c) if c == '<' || c == '^' || c == '>' => {
+ if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() {
+ match c {
+ '<' => *align = Alignment::Left,
+ '^' => *align = Alignment::Center,
+ '>' => *align = Alignment::Right,
+ _ => (),
+ }
+ }
+
+ (Width, None)
+ }
+ (Align, c @ '0'..='9') => (Width, Some(c)),
+ (Align | Width, '!') => {
+ if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() {
+ *truncate = true;
+ }
+ (Width, None)
+ }
+ (Align, '.') => (FirstStyle, None),
+ (Align, '}') => (Literal, None),
+ (Width, c @ '0'..='9') => (Width, Some(c)),
+ (Width, '.') => (FirstStyle, None),
+ (Width, '}') => (Literal, None),
+ (FirstStyle, '/') => (AltStyle, None),
+ (FirstStyle, '}') => (Literal, None),
+ (FirstStyle, c) => (FirstStyle, Some(c)),
+ (AltStyle, '}') => (Literal, None),
+ (AltStyle, c) => (AltStyle, Some(c)),
+ (st, c) => return Err(TemplateError { next: c, state: st }),
+ };
+
+ match (state, new.0) {
+ (MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal(
+ TabExpandedString::new(mem::take(&mut buf).into(), tab_width),
+ )),
+ (Key, Align | Literal) if !buf.is_empty() => {
+ parts.push(TemplatePart::Placeholder {
+ key: mem::take(&mut buf),
+ align: Alignment::Left,
+ width: None,
+ truncate: false,
+ style: None,
+ alt_style: None,
+ });
+ }
+ (Width, FirstStyle | Literal) if !buf.is_empty() => {
+ if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() {
+ *width = Some(buf.parse().unwrap());
+ buf.clear();
+ }
+ }
+ (FirstStyle, AltStyle | Literal) if !buf.is_empty() => {
+ if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() {
+ *style = Some(Style::from_dotted_str(&buf));
+ buf.clear();
+ }
+ }
+ (AltStyle, Literal) if !buf.is_empty() => {
+ if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() {
+ *alt_style = Some(Style::from_dotted_str(&buf));
+ buf.clear();
+ }
+ }
+ (_, _) => (),
+ }
+
+ state = new.0;
+ if let Some(c) = new.1 {
+ buf.push(c);
+ }
+ }
+
+ if matches!(state, Literal | DoubleClose) && !buf.is_empty() {
+ parts.push(TemplatePart::Literal(TabExpandedString::new(
+ buf.into(),
+ tab_width,
+ )));
+ }
+
+ Ok(Self { parts })
+ }
+
+ fn from_str(s: &str) -> Result<Self, TemplateError> {
+ Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH)
+ }
+
+ fn set_tab_width(&mut self, new_tab_width: usize) {
+ for part in &mut self.parts {
+ if let TemplatePart::Literal(s) = part {
+ s.set_tab_width(new_tab_width);
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct TemplateError {
+ state: State,
+ next: char,
+}
+
+impl fmt::Display for TemplateError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "TemplateError: unexpected character {:?} in state {:?}",
+ self.next, self.state
+ )
+ }
+}
+
+impl std::error::Error for TemplateError {}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum TemplatePart {
+ Literal(TabExpandedString),
+ Placeholder {
+ key: String,
+ align: Alignment,
+ width: Option<u16>,
+ truncate: bool,
+ style: Option<Style>,
+ alt_style: Option<Style>,
+ },
+ NewLine,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum State {
+ Literal,
+ MaybeOpen,
+ DoubleClose,
+ Key,
+ Align,
+ Width,
+ FirstStyle,
+ AltStyle,
+}
+
+struct BarDisplay<'a> {
+ chars: &'a [Box<str>],
+ filled: usize,
+ cur: Option<usize>,
+ rest: console::StyledObject<RepeatedStringDisplay<'a>>,
+}
+
+impl<'a> fmt::Display for BarDisplay<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ for _ in 0..self.filled {
+ f.write_str(&self.chars[0])?;
+ }
+ if let Some(cur) = self.cur {
+ f.write_str(&self.chars[cur])?;
+ }
+ self.rest.fmt(f)
+ }
+}
+
+struct RepeatedStringDisplay<'a> {
+ str: &'a str,
+ num: usize,
+}
+
+impl<'a> fmt::Display for RepeatedStringDisplay<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ for _ in 0..self.num {
+ f.write_str(self.str)?;
+ }
+ Ok(())
+ }
+}
+
+struct PaddedStringDisplay<'a> {
+ str: &'a str,
+ width: usize,
+ align: Alignment,
+ truncate: bool,
+}
+
+impl<'a> fmt::Display for PaddedStringDisplay<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let cols = measure_text_width(self.str);
+ let excess = cols.saturating_sub(self.width);
+ if excess > 0 && !self.truncate {
+ return f.write_str(self.str);
+ } else if excess > 0 {
+ let (start, end) = match self.align {
+ Alignment::Left => (0, self.str.len() - excess),
+ Alignment::Right => (excess, self.str.len()),
+ Alignment::Center => (
+ excess / 2,
+ self.str.len() - excess.saturating_sub(excess / 2),
+ ),
+ };
+
+ return f.write_str(self.str.get(start..end).unwrap_or(self.str));
+ }
+
+ let diff = self.width.saturating_sub(cols);
+ let (left_pad, right_pad) = match self.align {
+ Alignment::Left => (0, diff),
+ Alignment::Right => (diff, 0),
+ Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)),
+ };
+
+ for _ in 0..left_pad {
+ f.write_char(' ')?;
+ }
+ f.write_str(self.str)?;
+ for _ in 0..right_pad {
+ f.write_char(' ')?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+enum Alignment {
+ Left,
+ Center,
+ Right,
+}
+
+/// Trait for defining stateful or stateless formatters
+pub trait ProgressTracker: Send + Sync {
+ /// Creates a new instance of the progress tracker
+ fn clone_box(&self) -> Box<dyn ProgressTracker>;
+ /// Notifies the progress tracker of a tick event
+ fn tick(&mut self, state: &ProgressState, now: Instant);
+ /// Notifies the progress tracker of a reset event
+ fn reset(&mut self, state: &ProgressState, now: Instant);
+ /// Provides access to the progress bar display buffer for custom messages
+ fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write);
+}
+
+impl Clone for Box<dyn ProgressTracker> {
+ fn clone(&self) -> Self {
+ self.clone_box()
+ }
+}
+
+impl<F> ProgressTracker for F
+where
+ F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Sync + Clone + 'static,
+{
+ fn clone_box(&self) -> Box<dyn ProgressTracker> {
+ Box::new(self.clone())
+ }
+
+ fn tick(&mut self, _: &ProgressState, _: Instant) {}
+
+ fn reset(&mut self, _: &ProgressState, _: Instant) {}
+
+ fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) {
+ (self)(state, w);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+
+ use super::*;
+ use crate::state::{AtomicPosition, ProgressState};
+ use std::sync::Mutex;
+
+ #[test]
+ fn test_stateful_tracker() {
+ #[derive(Debug, Clone)]
+ struct TestTracker(Arc<Mutex<String>>);
+
+ impl ProgressTracker for TestTracker {
+ fn clone_box(&self) -> Box<dyn ProgressTracker> {
+ Box::new(self.clone())
+ }
+
+ fn tick(&mut self, state: &ProgressState, _: Instant) {
+ let mut m = self.0.lock().unwrap();
+ m.clear();
+ m.push_str(format!("{} {}", state.len().unwrap(), state.pos()).as_str());
+ }
+
+ fn reset(&mut self, _state: &ProgressState, _: Instant) {
+ let mut m = self.0.lock().unwrap();
+ m.clear();
+ }
+
+ fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) {
+ w.write_str(self.0.lock().unwrap().as_str()).unwrap();
+ }
+ }
+
+ use crate::ProgressBar;
+
+ let pb = ProgressBar::new(1);
+ pb.set_style(
+ ProgressStyle::with_template("{{ {foo} }}")
+ .unwrap()
+ .with_key("foo", TestTracker(Arc::new(Mutex::new(String::default()))))
+ .progress_chars("#>-"),
+ );
+
+ let mut buf = Vec::new();
+ let style = pb.clone().style();
+
+ style.format_state(&pb.state().state, &mut buf, 16);
+ assert_eq!(&buf[0], "{ }");
+ buf.clear();
+ pb.inc(1);
+ style.format_state(&pb.state().state, &mut buf, 16);
+ assert_eq!(&buf[0], "{ 1 1 }");
+ pb.reset();
+ buf.clear();
+ style.format_state(&pb.state().state, &mut buf, 16);
+ assert_eq!(&buf[0], "{ }");
+ pb.finish_and_clear();
+ }
+
+ use crate::state::TabExpandedString;
+
+ #[test]
+ fn test_expand_template() {
+ const WIDTH: u16 = 80;
+ let pos = Arc::new(AtomicPosition::new());
+ let state = ProgressState::new(Some(10), pos);
+ let mut buf = Vec::new();
+
+ let mut style = ProgressStyle::default_bar();
+ style.format_map.insert(
+ "foo",
+ Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO").unwrap()),
+ );
+ style.format_map.insert(
+ "bar",
+ Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR").unwrap()),
+ );
+
+ style.template = Template::from_str("{{ {foo} {bar} }}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "{ FOO BAR }");
+
+ buf.clear();
+ style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"#).unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"#);
+ }
+
+ #[test]
+ fn test_expand_template_flags() {
+ use console::set_colors_enabled;
+ set_colors_enabled(true);
+
+ const WIDTH: u16 = 80;
+ let pos = Arc::new(AtomicPosition::new());
+ let state = ProgressState::new(Some(10), pos);
+ let mut buf = Vec::new();
+
+ let mut style = ProgressStyle::default_bar();
+ style.format_map.insert(
+ "foo",
+ Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX").unwrap()),
+ );
+
+ style.template = Template::from_str("{foo:5}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "XXX ");
+
+ buf.clear();
+ style.template = Template::from_str("{foo:.red.on_blue}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m");
+
+ buf.clear();
+ style.template = Template::from_str("{foo:^5.red.on_blue}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
+
+ buf.clear();
+ style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
+ }
+
+ #[test]
+ fn align_truncation() {
+ const WIDTH: u16 = 10;
+ let pos = Arc::new(AtomicPosition::new());
+ let mut state = ProgressState::new(Some(10), pos);
+ let mut buf = Vec::new();
+
+ let style = ProgressStyle::with_template("{wide_msg}").unwrap();
+ state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "abcdefghij");
+
+ buf.clear();
+ let style = ProgressStyle::with_template("{wide_msg:>}").unwrap();
+ state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "klmnopqrst");
+
+ buf.clear();
+ let style = ProgressStyle::with_template("{wide_msg:^}").unwrap();
+ state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "fghijklmno");
+ }
+
+ #[test]
+ fn wide_element_style() {
+ const CHARS: &str = "=>-";
+ const WIDTH: u16 = 8;
+ let pos = Arc::new(AtomicPosition::new());
+ // half finished
+ pos.set(2);
+ let mut state = ProgressState::new(Some(4), pos);
+ let mut buf = Vec::new();
+
+ let style = ProgressStyle::with_template("{wide_bar}")
+ .unwrap()
+ .progress_chars(CHARS);
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "====>---");
+
+ buf.clear();
+ let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}")
+ .unwrap()
+ .progress_chars(CHARS);
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(
+ &buf[0],
+ "\u{1b}[31m\u{1b}[44m====>\u{1b}[32m\u{1b}[46m---\u{1b}[0m\u{1b}[0m"
+ );
+
+ buf.clear();
+ let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap();
+ state.message = TabExpandedString::NoTabs("foobar".into());
+ style.format_state(&state, &mut buf, WIDTH);
+ assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m foobar \u{1b}[0m");
+ }
+
+ #[test]
+ fn multiline_handling() {
+ const WIDTH: u16 = 80;
+ let pos = Arc::new(AtomicPosition::new());
+ let mut state = ProgressState::new(Some(10), pos);
+ let mut buf = Vec::new();
+
+ let mut style = ProgressStyle::default_bar();
+ state.message = TabExpandedString::new("foo\nbar\nbaz".into(), 2);
+ style.template = Template::from_str("{msg}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+
+ assert_eq!(buf.len(), 3);
+ assert_eq!(&buf[0], "foo");
+ assert_eq!(&buf[1], "bar");
+ assert_eq!(&buf[2], "baz");
+
+ buf.clear();
+ style.template = Template::from_str("{wide_msg}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+
+ assert_eq!(buf.len(), 3);
+ assert_eq!(&buf[0], "foo");
+ assert_eq!(&buf[1], "bar");
+ assert_eq!(&buf[2], "baz");
+
+ buf.clear();
+ state.prefix = TabExpandedString::new("prefix\nprefix".into(), 2);
+ style.template = Template::from_str("{prefix} {wide_msg}").unwrap();
+ style.format_state(&state, &mut buf, WIDTH);
+
+ assert_eq!(buf.len(), 4);
+ assert_eq!(&buf[0], "prefix");
+ assert_eq!(&buf[1], "prefix foo");
+ assert_eq!(&buf[2], "bar");
+ assert_eq!(&buf[3], "baz");
+ }
+}