From 494c89195bd15f603b36747b4d92673f21a59db8 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 11:54:50 +0200 Subject: [PATCH 1/9] Use higher-level API for template expansion tests --- src/style.rs | 72 +++++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/src/style.rs b/src/style.rs index ebe6d97e..ac6f4ef3 100644 --- a/src/style.rs +++ b/src/style.rs @@ -502,55 +502,47 @@ enum Alignment { #[cfg(test)] mod tests { use super::*; + use crate::draw_target::ProgressDrawTarget; + use crate::state::ProgressState; #[test] fn test_expand_template() { - let rv = expand_template("{{ {foo} {bar} }}", |var| var.key.to_uppercase()); - assert_eq!(&rv, "{ FOO BAR }"); - let rv = expand_template(r#"{ "foo": "{foo}", "bar": {bar} }"#, |var| { - var.key.to_uppercase() - }); - assert_eq!(&rv, r#"{ "foo": "FOO", "bar": BAR }"#); + let mut style = ProgressStyle::default_bar(); + style.format_map.0.insert("foo", |_| "FOO".into()); + style.format_map.0.insert("bar", |_| "BAR".into()); + let state = ProgressState::new(10, ProgressDrawTarget::stdout()); + + style.template = "{{ {foo} {bar} }}".into(); + let rv = style.format_state(&state); + assert_eq!(&rv[0], "{ FOO BAR }"); + + style.template = r#"{ "foo": "{foo}", "bar": {bar} }"#.into(); + let rv = style.format_state(&state); + assert_eq!(&rv[0], r#"{ "foo": "FOO", "bar": BAR }"#); } #[test] fn test_expand_template_flags() { use console::set_colors_enabled; set_colors_enabled(true); + let mut style = ProgressStyle::default_bar(); + style.format_map.0.insert("foo", |_| "XXX".into()); + let state = ProgressState::new(10, ProgressDrawTarget::stdout()); - let rv = expand_template("{foo:5}", |var| { - assert_eq!(var.key, "foo"); - assert_eq!(var.width, Some(5)); - "XXX".into() - }); - assert_eq!(&rv, "XXX "); - - let rv = expand_template("{foo:.red.on_blue}", |var| { - assert_eq!(var.key, "foo"); - assert_eq!(var.width, None); - assert_eq!(var.align, Alignment::Left); - assert_eq!(var.style, Some(Style::new().red().on_blue())); - "XXX".into() - }); - assert_eq!(&rv, "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m"); - - let rv = expand_template("{foo:^5.red.on_blue}", |var| { - assert_eq!(var.key, "foo"); - assert_eq!(var.width, Some(5)); - assert_eq!(var.align, Alignment::Center); - assert_eq!(var.style, Some(Style::new().red().on_blue())); - "XXX".into() - }); - assert_eq!(&rv, "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m"); - - let rv = expand_template("{foo:^5.red.on_blue/green.on_cyan}", |var| { - assert_eq!(var.key, "foo"); - assert_eq!(var.width, Some(5)); - assert_eq!(var.align, Alignment::Center); - assert_eq!(var.style, Some(Style::new().red().on_blue())); - assert_eq!(var.alt_style, Some(Style::new().green().on_cyan())); - "XXX".into() - }); - assert_eq!(&rv, "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m"); + style.template = "{foo:5}".into(); + let rv = style.format_state(&state); + assert_eq!(&rv[0], "XXX "); + + style.template = "{foo:.red.on_blue}".into(); + let rv = style.format_state(&state); + assert_eq!(&rv[0], "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m"); + + style.template = "{foo:^5.red.on_blue}".into(); + let rv = style.format_state(&state); + assert_eq!(&rv[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m"); + + style.template = "{foo:^5.red.on_blue/green.on_cyan}".into(); + let rv = style.format_state(&state); + assert_eq!(&rv[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m"); } } From 73c8f92cc8d766d30063a434543b4c3f8a3fc793 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 12:03:06 +0200 Subject: [PATCH 2/9] Make padding and truncation lazy --- src/style.rs | 88 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/src/style.rs b/src/style.rs index ac6f4ef3..0e7497de 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::fmt::{self, Write}; use console::{measure_text_width, Style}; use once_cell::sync::Lazy; @@ -300,7 +301,13 @@ impl ProgressStyle { ) } 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); + let msg = PaddedStringDisplay { + str: state.message(), + width: msg_width, + align: var.align, + truncate: true, + } + .to_string(); s.replace( "\x00", if var.last_element { @@ -379,45 +386,62 @@ fn expand_template) -> String>(s: &str, mut f: F) -> C var.alt_style = Some(Style::from_dotted_str(alt_style.as_str())); } } - let mut rv = f(&var); - if let Some(width) = var.width { - rv = pad_str(&rv, width, var.align, var.truncate).to_string() - } - if let Some(s) = var.style { - rv = s.apply_to(rv).to_string(); + + let rv = f(&var); + match var.width { + Some(width) => { + let padded = PaddedStringDisplay { + str: &rv, + width, + align: var.align, + truncate: var.truncate, + }; + match var.style { + Some(s) => s.apply_to(padded).to_string(), + None => padded.to_string(), + } + } + None => match var.style { + Some(s) => s.apply_to(rv).to_string(), + None => rv, + }, } - rv }) } -fn pad_str(s: &str, width: usize, align: Alignment, truncate: bool) -> Cow<'_, str> { - let cols = measure_text_width(s); - - if cols >= width { - return if truncate { - Cow::Borrowed(s.get(..width).unwrap_or(s)) - } else { - Cow::Borrowed(s) - }; - } +struct PaddedStringDisplay<'a> { + str: &'a str, + width: usize, + align: Alignment, + truncate: bool, +} - let diff = width.saturating_sub(cols); +impl<'a> fmt::Display for PaddedStringDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cols = measure_text_width(self.str); + if cols >= self.width { + return match self.truncate { + true => f.write_str(self.str.get(..self.width).unwrap_or(self.str)), + false => f.write_str(self.str), + }; + } - let (left_pad, right_pad) = match align { - Alignment::Left => (0, diff), - Alignment::Right => (diff, 0), - Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)), - }; + 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)), + }; - let mut rv = String::new(); - for _ in 0..left_pad { - rv.push(' '); - } - rv.push_str(s); - for _ in 0..right_pad { - rv.push(' '); + for _ in 0..left_pad { + f.write_char(' ')?; + } + f.write_str(self.str)?; + for _ in 0..right_pad { + f.write_char(' ')?; + } + Ok(()) } - Cow::Owned(rv) } #[derive(Clone, Default)] From b5372bb74f3134d941bcacf2a6cc2fbd3e92169c Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 12:18:40 +0200 Subject: [PATCH 3/9] Inline expand_template() --- src/style.rs | 239 +++++++++++++++++++++++++-------------------------- 1 file changed, 118 insertions(+), 121 deletions(-) diff --git a/src/style.rs b/src/style.rs index 0e7497de..dd95a3e8 100644 --- a/src/style.rs +++ b/src/style.rs @@ -238,56 +238,134 @@ impl ProgressStyle { } pub(crate) fn format_state(&self, state: &ProgressState) -> Vec { - let mut rv = vec![]; + 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_]+)*))? + )? + ", + ) + .unwrap() + }); + let mut rv = vec![]; for line in self.template.lines() { let mut wide_element = None; + let s = VAR_RE.replace_all(line, |caps: &Captures<'_>| { + if caps.get(1).is_some() { + return "}".into(); + } - let s = expand_template(line, |var| { - if let Some(formatter) = self.format_map.0.get(var.key) { - return formatter(state); + let key = &caps[2]; + if key == "{" { + return "{".into(); } - match var.key { - "wide_bar" => { - wide_element = Some(var.duplicate_for_key("bar")); - "\x00".into() + let mut var = TemplateVar { + key, + align: Alignment::Left, + truncate: false, + width: None, + style: None, + alt_style: None, + last_element: caps.get(0).unwrap().end() >= line.len(), + }; + + if let Some(opt_caps) = KEY_RE.captures(&key[..key.len() - 1]) { + if let Some(short_key) = opt_caps.get(1) { + var.key = short_key.as_str(); } - "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() + var.align = match opt_caps.get(2).map(|x| x.as_str()) { + Some("<") => Alignment::Left, + Some("^") => Alignment::Center, + Some(">") => Alignment::Right, + _ => Alignment::Left, + }; + if let Some(width) = opt_caps.get(3) { + var.width = Some(width.as_str().parse().unwrap()); } - "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())) + if opt_caps.get(4).is_some() { + var.truncate = true; } - "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)) + if let Some(style) = opt_caps.get(5) { + var.style = Some(Style::from_dotted_str(style.as_str())); + } + if let Some(alt_style) = opt_caps.get(6) { + var.alt_style = Some(Style::from_dotted_str(alt_style.as_str())); } - "eta_precise" => format!("{}", FormattedDuration(state.eta())), - "eta" => format!("{:#}", HumanDuration(state.eta())), - "duration_precise" => format!("{}", FormattedDuration(state.duration())), - "duration" => format!("{:#}", HumanDuration(state.duration())), - _ => "".into(), + } + + let rv = if let Some(formatter) = self.format_map.0.get(var.key) { + formatter(state) + } else { + 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)) + } + "eta_precise" => format!("{}", FormattedDuration(state.eta())), + "eta" => format!("{:#}", HumanDuration(state.eta())), + "duration_precise" => format!("{}", FormattedDuration(state.duration())), + "duration" => format!("{:#}", HumanDuration(state.duration())), + _ => "".into(), + } + }; + + match var.width { + Some(width) => { + let padded = PaddedStringDisplay { + str: &rv, + width, + align: var.align, + truncate: var.truncate, + }; + match var.style { + Some(s) => s.apply_to(padded).to_string(), + None => padded.to_string(), + } + } + None => match var.style { + Some(s) => s.apply_to(rv).to_string(), + None => rv, + }, } }); @@ -328,87 +406,6 @@ impl ProgressStyle { } } -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_]+)*))? - )? - ", - ) - .unwrap() - }); - VAR_RE.replace_all(s, |caps: &Captures<'_>| { - if caps.get(1).is_some() { - return "}".into(); - } - let key = &caps[2]; - if key == "{" { - return "{".into(); - } - let mut var = TemplateVar { - key, - align: Alignment::Left, - truncate: false, - width: None, - style: None, - alt_style: None, - last_element: caps.get(0).unwrap().end() >= s.len(), - }; - if let Some(opt_caps) = KEY_RE.captures(&key[..key.len() - 1]) { - if let Some(short_key) = opt_caps.get(1) { - var.key = short_key.as_str(); - } - var.align = match opt_caps.get(2).map(|x| x.as_str()) { - Some("<") => Alignment::Left, - Some("^") => Alignment::Center, - Some(">") => Alignment::Right, - _ => Alignment::Left, - }; - if let Some(width) = opt_caps.get(3) { - var.width = Some(width.as_str().parse().unwrap()); - } - if opt_caps.get(4).is_some() { - var.truncate = true; - } - if let Some(style) = opt_caps.get(5) { - var.style = Some(Style::from_dotted_str(style.as_str())); - } - if let Some(alt_style) = opt_caps.get(6) { - var.alt_style = Some(Style::from_dotted_str(alt_style.as_str())); - } - } - - let rv = f(&var); - match var.width { - Some(width) => { - let padded = PaddedStringDisplay { - str: &rv, - width, - align: var.align, - truncate: var.truncate, - }; - match var.style { - Some(s) => s.apply_to(padded).to_string(), - None => padded.to_string(), - } - } - None => match var.style { - Some(s) => s.apply_to(rv).to_string(), - None => rv, - }, - } - }) -} - struct PaddedStringDisplay<'a> { str: &'a str, width: usize, From a200e4e20a38940ddfcce9bc490e015f6c37ff8b Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 12:54:41 +0200 Subject: [PATCH 4/9] Lazily draw progress bars --- src/style.rs | 78 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/src/style.rs b/src/style.rs index dd95a3e8..49a7491c 100644 --- a/src/style.rs +++ b/src/style.rs @@ -192,7 +192,12 @@ 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). @@ -207,8 +212,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); @@ -221,20 +224,24 @@ 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 { @@ -311,10 +318,13 @@ impl ProgressStyle { 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(), + "bar" => format!( + "{}", + self.format_bar( + state.fraction(), + var.width.unwrap_or(20), + var.alt_style.as_ref(), + ) ), "spinner" => state.current_tick_str().to_string(), "wide_msg" => { @@ -375,7 +385,10 @@ impl ProgressStyle { 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()), + &format!( + "{}", + 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)); @@ -406,6 +419,39 @@ impl ProgressStyle { } } +struct BarDisplay<'a> { + chars: &'a [Box], + filled: usize, + cur: Option, + rest: console::StyledObject>, +} + +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, From ab21f3e76276c8854556452ff92f73e6a860892e Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 12:57:25 +0200 Subject: [PATCH 5/9] Deduplicate ProgressStyle initialization code --- src/style.rs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/style.rs b/src/style.rs index 49a7491c..352795a2 100644 --- a/src/style.rs +++ b/src/style.rs @@ -63,23 +63,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: impl Into>) -> Self { let progress_chars = segment("█░"); let char_width = width(&progress_chars); ProgressStyle { @@ -89,7 +81,7 @@ impl ProgressStyle { .collect(), progress_chars, char_width, - template: "{spinner} {msg}".into(), + template: template.into(), on_finish: ProgressFinish::default(), format_map: FormatMap::default(), } @@ -192,12 +184,7 @@ impl ProgressStyle { &self.on_finish } - fn format_bar( - &self, - fract: f32, - width: usize, - alt_style: Option<&Style>, - ) -> BarDisplay<'_> { + 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). From 841927e353ca49e5bf40495a8c5d5adf8405553c Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 12:58:16 +0200 Subject: [PATCH 6/9] Remove deprecated methods --- src/style.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/style.rs b/src/style.rs index 352795a2..4b300f3b 100644 --- a/src/style.rs +++ b/src/style.rs @@ -157,23 +157,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(' ') - } - /// 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 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] From a040665545945a977a4e7e0f2857e7e46998b3a2 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 13:01:19 +0200 Subject: [PATCH 7/9] Move current_tick_str() logic into ProgressStyle --- src/state.rs | 6 +----- src/style.rs | 7 +++++++ 2 files changed, 8 insertions(+), 5 deletions(-) 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 4b300f3b..075e28c5 100644 --- a/src/style.rs +++ b/src/style.rs @@ -157,6 +157,13 @@ impl ProgressStyle { self } + 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 pub fn get_tick_str(&self, idx: u64) -> &str { &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)] From 1fe41d50533cb12be3ec23987808644165fbd46f Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 13:24:46 +0200 Subject: [PATCH 8/9] Reuse buffer for template expansions --- src/style.rs | 117 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/src/style.rs b/src/style.rs index 075e28c5..6442854c 100644 --- a/src/style.rs +++ b/src/style.rs @@ -245,6 +245,7 @@ impl ProgressStyle { .unwrap() }); + let mut buf = String::new(); let mut rv = vec![]; for line in self.template.lines() { let mut wide_element = None; @@ -292,59 +293,93 @@ impl ProgressStyle { } } - let rv = if let Some(formatter) = self.format_map.0.get(var.key) { - formatter(state) + buf.clear(); + if let Some(formatter) = self.format_map.0.get(var.key) { + buf.push_str(&formatter(state)); } else { match var.key { "wide_bar" => { wide_element = Some(var.duplicate_for_key("bar")); - "\x00".into() + buf.push_str("\x00"); } - "bar" => format!( - "{}", - self.format_bar( - state.fraction(), - var.width.unwrap_or(20), - var.alt_style.as_ref(), - ) - ), - "spinner" => state.current_tick_str().to_string(), + "bar" => buf + .write_fmt(format_args!( + "{}", + self.format_bar( + state.fraction(), + var.width.unwrap_or(20), + var.alt_style.as_ref(), + ) + )) + .unwrap(), + "spinner" => buf.push_str(state.current_tick_str()), "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)) + buf.push_str("\x00"); } - "eta_precise" => format!("{}", FormattedDuration(state.eta())), - "eta" => format!("{:#}", HumanDuration(state.eta())), - "duration_precise" => format!("{}", FormattedDuration(state.duration())), - "duration" => format!("{:#}", HumanDuration(state.duration())), - _ => "".into(), + "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 var.width { Some(width) => { let padded = PaddedStringDisplay { - str: &rv, + str: &buf, width, align: var.align, truncate: var.truncate, @@ -355,8 +390,8 @@ impl ProgressStyle { } } None => match var.style { - Some(s) => s.apply_to(rv).to_string(), - None => rv, + Some(s) => s.apply_to(&buf).to_string(), + None => buf.clone(), }, } }); From 11e33cba88aa405d312f48c0a63b22cda43d7889 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 28 Oct 2021 13:57:20 +0200 Subject: [PATCH 9/9] Replace regex with hand-written parser --- Cargo.toml | 3 +- src/style.rs | 567 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 342 insertions(+), 228 deletions(-) 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/style.rs b/src/style.rs index 6442854c..f9358319 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,10 +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; @@ -16,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, @@ -71,7 +70,7 @@ impl ProgressStyle { Self::new("{spinner} {msg}") } - fn new(template: impl Into>) -> Self { + fn new(template: &str) -> Self { let progress_chars = segment("█░"); let char_width = width(&progress_chars); ProgressStyle { @@ -81,7 +80,7 @@ impl ProgressStyle { .collect(), progress_chars, char_width, - template: template.into(), + template: Template::from_str(template), on_finish: ProgressFinish::default(), format_map: FormatMap::default(), } @@ -137,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 } @@ -227,215 +226,356 @@ impl ProgressStyle { } pub(crate) fn format_state(&self, state: &ProgressState) -> Vec { - 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_]+)*))? - )? - ", - ) - .unwrap() - }); - + 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 = VAR_RE.replace_all(line, |caps: &Captures<'_>| { - if caps.get(1).is_some() { - return "}".into(); - } - - let key = &caps[2]; - if key == "{" { - return "{".into(); - } - - let mut var = TemplateVar { + let mut wide_element = None; + for part in &self.template.parts { + match part { + TemplatePart::Placeholder { key, - align: Alignment::Left, - truncate: false, - width: None, - style: None, - alt_style: None, - last_element: caps.get(0).unwrap().end() >= line.len(), - }; - - if let Some(opt_caps) = KEY_RE.captures(&key[..key.len() - 1]) { - if let Some(short_key) = opt_caps.get(1) { - var.key = short_key.as_str(); - } - var.align = match opt_caps.get(2).map(|x| x.as_str()) { - Some("<") => Alignment::Left, - Some("^") => Alignment::Center, - Some(">") => Alignment::Right, - _ => Alignment::Left, + 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(), + _ => (), + } }; - if let Some(width) = opt_caps.get(3) { - var.width = Some(width.as_str().parse().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), + }, } - if opt_caps.get(4).is_some() { - var.truncate = true; + } + TemplatePart::Literal(s) => cur.push_str(s), + TemplatePart::NewLine => { + rv.push(expand_wide( + mem::take(&mut cur), + wide_element.take(), + self, + state, + &mut buf, + )); + } + } + } + + if !cur.is_empty() { + rv.push(expand_wide( + mem::take(&mut cur), + wide_element.take(), + self, + state, + &mut buf, + )); + } + rv + } +} + +fn expand_wide( + cur: String, + wide: Option<(&str, &Alignment, &Option