diff --git a/Cargo.toml b/Cargo.toml index a03c4d24..a7aea7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,6 @@ edition = "2018" exclude = ["screenshots/*"] [dependencies] -regex = { version = "1.3.1", default-features = false, features = ["std"] } -once_cell = "1.8.0" number_prefix = "0.4" console = { version = "0.15.0", default-features = false, features = ["ansi-parsing"] } unicode-segmentation = { version = "1.6.0", optional = true } @@ -23,6 +21,7 @@ rayon = { version = "1.0", optional = true } tokio = { version = "1.0", optional = true, features = ["fs", "io-util"] } [dev-dependencies] +once_cell = "1.8.0" rand = "0.8" structopt = "0.3" tokio = { version = "1.0", features = ["time", "rt"] } diff --git a/src/state.rs b/src/state.rs index 34fb73a2..b51f0afc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -50,11 +50,7 @@ impl ProgressState { /// Returns the string that should be drawn for the /// current spinner string. pub fn current_tick_str(&self) -> &str { - if self.is_finished() { - self.style.get_final_tick_str() - } else { - self.style.get_tick_str(self.tick) - } + self.style.current_tick_str(self) } /// Indicates that the progress bar finished. diff --git a/src/style.rs b/src/style.rs index ebe6d97e..f9358319 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::fmt::{self, Write}; +use std::mem; use console::{measure_text_width, Style}; -use once_cell::sync::Lazy; -use regex::{Captures, Regex}; #[cfg(feature = "improved_unicode")] use unicode_segmentation::UnicodeSegmentation; @@ -15,7 +15,7 @@ use crate::state::ProgressState; pub struct ProgressStyle { tick_strings: Vec>, progress_chars: Vec>, - template: Box, + template: Template, on_finish: ProgressFinish, // how unicode-big each char in progress_chars is char_width: usize, @@ -62,23 +62,15 @@ fn width(c: &[Box]) -> usize { impl ProgressStyle { /// Returns the default progress bar style for bars pub fn default_bar() -> ProgressStyle { - let progress_chars = segment("█░"); - let char_width = width(&progress_chars); - ProgressStyle { - tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ " - .chars() - .map(|c| c.to_string().into()) - .collect(), - progress_chars, - char_width, - template: "{wide_bar} {pos}/{len}".into(), - on_finish: ProgressFinish::default(), - format_map: FormatMap::default(), - } + Self::new("{wide_bar} {pos}/{len}") } /// Returns the default progress bar style for spinners - pub fn default_spinner() -> ProgressStyle { + pub fn default_spinner() -> Self { + Self::new("{spinner} {msg}") + } + + fn new(template: &str) -> Self { let progress_chars = segment("█░"); let char_width = width(&progress_chars); ProgressStyle { @@ -88,7 +80,7 @@ impl ProgressStyle { .collect(), progress_chars, char_width, - template: "{spinner} {msg}".into(), + template: Template::from_str(template), on_finish: ProgressFinish::default(), format_map: FormatMap::default(), } @@ -144,7 +136,7 @@ impl ProgressStyle { /// /// Review the [list of template keys](./index.html#templates) for more information. pub fn template(mut self, s: &str) -> ProgressStyle { - self.template = s.into(); + self.template = Template::from_str(s); self } @@ -164,10 +156,11 @@ impl ProgressStyle { self } - /// Returns the tick char for a given number - #[deprecated(since = "0.13.0", note = "Deprecated in favor of get_tick_str")] - pub fn get_tick_char(&self, idx: u64) -> char { - self.get_tick_str(idx).chars().next().unwrap_or(' ') + pub(crate) 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 @@ -175,12 +168,6 @@ impl ProgressStyle { &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)] } - /// Returns the tick char for the finished state - #[deprecated(since = "0.13.0", note = "Deprecated in favor of get_final_tick_str")] - pub fn get_final_tick_char(&self) -> char { - self.get_final_tick_str().chars().next().unwrap_or(' ') - } - /// Returns the tick string for the finished state pub fn get_final_tick_str(&self) -> &str { &self.tick_strings[self.tick_strings.len() - 1] @@ -191,7 +178,7 @@ impl ProgressStyle { &self.on_finish } - pub(crate) fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> String { + 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). @@ -206,8 +193,6 @@ impl ProgressStyle { 0 }; - let pb = self.progress_chars[0].repeat(entirely_filled); - let cur = if head == 1 { // Number of fine-grained progress entries in progress_chars. let n = self.progress_chars.len().saturating_sub(2); @@ -220,204 +205,443 @@ impl ProgressStyle { // 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) }; - self.progress_chars[cur_char].to_string() + Some(cur_char) } else { - "".into() + 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 = self.progress_chars[self.progress_chars.len() - 1].repeat(bg); - format!( - "{}{}{}", - pb, + let rest = RepeatedStringDisplay { + str: &self.progress_chars[self.progress_chars.len() - 1], + num: bg, + }; + + BarDisplay { + chars: &self.progress_chars, + filled: entirely_filled, cur, - alt_style.unwrap_or(&Style::new()).apply_to(rest) - ) + rest: alt_style.unwrap_or(&Style::new()).apply_to(rest), + } } pub(crate) fn format_state(&self, state: &ProgressState) -> Vec { + let mut cur = String::new(); + let mut buf = String::new(); let mut rv = vec![]; - - for line in self.template.lines() { - let mut wide_element = None; - - let s = expand_template(line, |var| { - if let Some(formatter) = self.format_map.0.get(var.key) { - return formatter(state); - } - - match var.key { - "wide_bar" => { - wide_element = Some(var.duplicate_for_key("bar")); - "\x00".into() - } - "bar" => self.format_bar( - state.fraction(), - var.width.unwrap_or(20), - var.alt_style.as_ref(), - ), - "spinner" => state.current_tick_str().to_string(), - "wide_msg" => { - wide_element = Some(var.duplicate_for_key("msg")); - "\x00".into() - } - "msg" => state.message().to_string(), - "prefix" => state.prefix().to_string(), - "pos" => state.pos.to_string(), - "len" => state.len.to_string(), - "percent" => format!("{:.*}", 0, state.fraction() * 100f32), - "bytes" => format!("{}", HumanBytes(state.pos)), - "total_bytes" => format!("{}", HumanBytes(state.len)), - "decimal_bytes" => format!("{}", DecimalBytes(state.pos)), - "decimal_total_bytes" => format!("{}", DecimalBytes(state.len)), - "binary_bytes" => format!("{}", BinaryBytes(state.pos)), - "binary_total_bytes" => format!("{}", BinaryBytes(state.len)), - "elapsed_precise" => { - format!("{}", FormattedDuration(state.started.elapsed())) - } - "elapsed" => format!("{:#}", HumanDuration(state.started.elapsed())), - "per_sec" => format!("{:.4}/s", state.per_sec()), - "bytes_per_sec" => format!("{}/s", HumanBytes(state.per_sec() as u64)), - "binary_bytes_per_sec" => { - format!("{}/s", BinaryBytes(state.per_sec() as u64)) + let mut wide_element = None; + for part in &self.template.parts { + match part { + TemplatePart::Placeholder { + key, + align, + width, + truncate, + style, + alt_style, + } => { + buf.clear(); + if let Some(formatter) = self.format_map.0.get(key.as_str()) { + buf.push_str(&formatter(state)); + } else { + match key.as_str() { + "wide_bar" => { + wide_element = Some((key.as_str(), align, 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(state.current_tick_str()), + "wide_msg" => { + wide_element = Some((key.as_str(), align, alt_style)); + buf.push('\x00'); + } + "msg" => buf.push_str(state.message()), + "prefix" => buf.push_str(state.prefix()), + "pos" => buf.write_fmt(format_args!("{}", state.pos)).unwrap(), + "len" => buf.write_fmt(format_args!("{}", state.len)).unwrap(), + "percent" => buf + .write_fmt(format_args!("{:.*}", 0, state.fraction() * 100f32)) + .unwrap(), + "bytes" => buf + .write_fmt(format_args!("{}", HumanBytes(state.pos))) + .unwrap(), + "total_bytes" => buf + .write_fmt(format_args!("{}", HumanBytes(state.len))) + .unwrap(), + "decimal_bytes" => buf + .write_fmt(format_args!("{}", DecimalBytes(state.pos))) + .unwrap(), + "decimal_total_bytes" => buf + .write_fmt(format_args!("{}", DecimalBytes(state.len))) + .unwrap(), + "binary_bytes" => buf + .write_fmt(format_args!("{}", BinaryBytes(state.pos))) + .unwrap(), + "binary_total_bytes" => buf + .write_fmt(format_args!("{}", BinaryBytes(state.len))) + .unwrap(), + "elapsed_precise" => buf + .write_fmt(format_args!( + "{}", + FormattedDuration(state.started.elapsed()) + )) + .unwrap(), + "elapsed" => buf + .write_fmt(format_args!( + "{:#}", + HumanDuration(state.started.elapsed()) + )) + .unwrap(), + "per_sec" => buf + .write_fmt(format_args!("{:.4}/s", 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), + }, } - "eta_precise" => format!("{}", FormattedDuration(state.eta())), - "eta" => format!("{:#}", HumanDuration(state.eta())), - "duration_precise" => format!("{}", FormattedDuration(state.duration())), - "duration" => format!("{:#}", HumanDuration(state.duration())), - _ => "".into(), } - }); - - rv.push(if let Some(ref var) = wide_element { - let total_width = state.width(); - if var.key == "bar" { - let bar_width = total_width.saturating_sub(measure_text_width(&s)); - s.replace( - "\x00", - &self.format_bar(state.fraction(), bar_width, var.alt_style.as_ref()), - ) - } else if var.key == "msg" { - let msg_width = total_width.saturating_sub(measure_text_width(&s)); - let msg = pad_str(state.message(), msg_width, var.align, true); - s.replace( - "\x00", - if var.last_element { - msg.trim_end() - } else { - &msg - }, - ) - } else { - unreachable!() + TemplatePart::Literal(s) => cur.push_str(s), + TemplatePart::NewLine => { + rv.push(expand_wide( + mem::take(&mut cur), + wide_element.take(), + self, + state, + &mut buf, + )); } - } else { - s.to_string() - }); + } } + if !cur.is_empty() { + rv.push(expand_wide( + mem::take(&mut cur), + wide_element.take(), + self, + state, + &mut buf, + )); + } rv } } -fn expand_template) -> String>(s: &str, mut f: F) -> Cow<'_, str> { - static VAR_RE: Lazy = Lazy::new(|| Regex::new(r"(\}\})|\{(\{|[^{}}]+\})").unwrap()); - static KEY_RE: Lazy = Lazy::new(|| { - Regex::new( - r"(?x) - ([^:]+) - (?: - : - ([<^>])? - ([0-9]+)? - (!)? - (?:\.([0-9a-z_]+(?:\.[0-9a-z_]+)*))? - (?:/([a-z_]+(?:\.[a-z_]+)*))? - )? - ", +fn expand_wide( + cur: String, + wide: Option<(&str, &Alignment, &Option