diff options
author | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
---|---|---|
committer | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
commit | 1b6a04ca5504955c571d1c97504fb45ea0befee4 (patch) | |
tree | 7579f518b23313e8a9748a88ab6173d5e030b227 /vendor/indicatif/src/style.rs | |
parent | 5ecd8cf2cba827454317368b68571df0d13d7842 (diff) | |
download | fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.tar.xz fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.zip |
Initial vendor packages
Signed-off-by: Valentin Popov <valentin@popov.link>
Diffstat (limited to 'vendor/indicatif/src/style.rs')
-rw-r--r-- | vendor/indicatif/src/style.rs | 987 |
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"); + } +} |