From 72c25c13f2cc802e64e8b2c7b787bdddb10312a8 Mon Sep 17 00:00:00 2001 From: Chris Laplante Date: Thu, 28 Apr 2022 12:22:34 -0400 Subject: [PATCH 1/3] move message/prefix into the state It makes more sense here anyway, plus it gets rid of the mem::swap stuff --- src/progress_bar.rs | 51 ++++++++++++++++++++++++++++++++++++++------- src/state.rs | 12 +++++++---- src/style.rs | 36 ++++++++++++++------------------ 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/progress_bar.rs b/src/progress_bar.rs index b5a3915c..743d560c 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -3,8 +3,9 @@ use std::borrow::Cow; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak}; use std::time::{Duration, Instant}; -use std::{fmt, io, mem, thread}; +use std::{fmt, io, thread}; +use console::strip_ansi_codes; #[cfg(test)] use once_cell::sync::Lazy; @@ -71,13 +72,13 @@ impl ProgressBar { /// A convenience builder-like function for a progress bar with a given prefix pub fn with_prefix(self, prefix: impl Into>) -> ProgressBar { - self.state().style.prefix = prefix.into(); + self.state().state.prefix = prefix.into(); self } /// A convenience builder-like function for a progress bar with a given message pub fn with_message(self, message: impl Into>) -> ProgressBar { - self.state().style.message = message.into(); + self.state().state.message = message.into(); self } @@ -121,11 +122,8 @@ impl ProgressBar { /// Overrides the stored style /// /// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it. - pub fn set_style(&self, mut style: ProgressStyle) { - let mut state = self.state(); - mem::swap(&mut state.style.message, &mut style.message); - mem::swap(&mut state.style.prefix, &mut style.prefix); - state.style = style; + pub fn set_style(&self, style: ProgressStyle) { + self.state().style = style; } /// Spawns a background thread to tick the progress bar @@ -517,6 +515,26 @@ impl ProgressBar { self.state().draw_target.remote().map(|(_, idx)| idx) } + /// Current message + pub fn message(&self) -> Cow<'static, str> { + self.state().state.message.clone() + } + + /// Current message (with ANSI escape codes stripped) + pub fn message_unstyled(&self) -> String { + strip_ansi_codes(&self.message()).to_string() + } + + /// Current prefix + pub fn prefix(&self) -> Cow<'static, str> { + self.state().state.prefix.clone() + } + + /// Current prefix (with ANSI escape codes stripped) + pub fn prefix_unstyled(&self) -> String { + strip_ansi_codes(&self.prefix()).to_string() + } + #[inline] pub(crate) fn state(&self) -> MutexGuard<'_, BarState> { self.state.lock().unwrap() @@ -644,6 +662,7 @@ pub(crate) static TICKER_TEST: Lazy> = Lazy::new(Mutex::default); #[cfg(test)] mod tests { use super::*; + use console::Style; #[allow(clippy::float_cmp)] #[test] @@ -743,4 +762,20 @@ mod tests { drop(pb2); assert!(!TICKER_RUNNING.load(Ordering::SeqCst)); } + + #[test] + fn access_message_and_prefix() { + let pb = ProgressBar::new(80); + pb.set_message(Style::new().red().bold().apply_to("text").to_string()); + pb.set_prefix( + Style::new() + .on_blue() + .italic() + .apply_to("prefix!") + .to_string(), + ); + + assert_eq!(pb.message_unstyled(), "text"); + assert_eq!(pb.prefix_unstyled(), "prefix!"); + } } diff --git a/src/state.rs b/src/state.rs index 964d98f8..b1d93531 100644 --- a/src/state.rs +++ b/src/state.rs @@ -42,7 +42,7 @@ impl BarState { if let Some(len) = self.state.len { self.state.pos.set(len); } - self.style.message = msg; + self.state.message = msg; } ProgressFinish::AndClear => { if let Some(len) = self.state.len { @@ -51,7 +51,7 @@ impl BarState { self.state.status = Status::DoneHidden; } ProgressFinish::Abandon => {} - ProgressFinish::AbandonWithMessage(msg) => self.style.message = msg, + ProgressFinish::AbandonWithMessage(msg) => self.state.message = msg, } // There's no need to update the estimate here; once the `status` is no longer @@ -93,12 +93,12 @@ impl BarState { } pub(crate) fn set_message(&mut self, now: Instant, msg: Cow<'static, str>) { - self.style.message = msg; + self.state.message = msg; self.update_estimate_and_draw(now); } pub(crate) fn set_prefix(&mut self, now: Instant, prefix: Cow<'static, str>) { - self.style.prefix = prefix; + self.state.prefix = prefix; self.update_estimate_and_draw(now); } @@ -190,6 +190,8 @@ pub struct ProgressState { pub(crate) started: Instant, status: Status, est: Estimator, + pub(crate) message: Cow<'static, str>, + pub(crate) prefix: Cow<'static, str>, } impl ProgressState { @@ -201,6 +203,8 @@ impl ProgressState { status: Status::InProgress, started: Instant::now(), est: Estimator::new(Instant::now()), + message: "".into(), + prefix: "".into(), } } diff --git a/src/style.rs b/src/style.rs index e8cd0860..ccadffc0 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; + use std::collections::HashMap; use std::fmt::{self, Write}; use std::mem; @@ -15,8 +15,6 @@ use crate::state::ProgressState; /// Controls the rendering style of progress bars #[derive(Clone)] pub struct ProgressStyle { - pub(crate) message: Cow<'static, str>, - pub(crate) prefix: Cow<'static, str>, tick_strings: Vec>, progress_chars: Vec>, template: Template, @@ -81,8 +79,6 @@ impl ProgressStyle { let progress_chars = segment("█░"); let char_width = width(&progress_chars); Self { - message: "".into(), - prefix: "".into(), tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ " .chars() .map(|c| c.to_string().into()) @@ -258,8 +254,8 @@ impl ProgressStyle { wide = Some(WideElement::Message { align }); buf.push('\x00'); } - "msg" => buf.push_str(&self.message), - "prefix" => buf.push_str(&self.prefix), + "msg" => buf.push_str(&state.message), + "prefix" => buf.push_str(&state.prefix), "pos" => buf.write_fmt(format_args!("{}", pos)).unwrap(), "human_pos" => { buf.write_fmt(format_args!("{}", HumanCount(pos))).unwrap() @@ -392,7 +388,7 @@ impl<'a> WideElement<'a> { buf.write_fmt(format_args!( "{}", PaddedStringDisplay { - str: &style.message, + str: &state.message, width: left, align: *align, truncate: true, @@ -558,7 +554,7 @@ impl fmt::Display for TemplateError { impl std::error::Error for TemplateError {} -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] enum TemplatePart { Literal(String), Placeholder { @@ -572,7 +568,7 @@ enum TemplatePart { NewLine, } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] enum State { Literal, MaybeOpen, @@ -733,23 +729,23 @@ mod tests { fn align_truncation() { const WIDTH: u16 = 10; let pos = Arc::new(AtomicPosition::new()); - let state = ProgressState::new(Some(10), pos); + let mut state = ProgressState::new(Some(10), pos); let mut buf = Vec::new(); - let mut style = ProgressStyle::with_template("{wide_msg}").unwrap(); - style.message = "abcdefghijklmnopqrst".into(); + let style = ProgressStyle::with_template("{wide_msg}").unwrap(); + state.message = "abcdefghijklmnopqrst".into(); style.format_state(&state, &mut buf, WIDTH); assert_eq!(&buf[0], "abcdefghij"); buf.clear(); - let mut style = ProgressStyle::with_template("{wide_msg:>}").unwrap(); - style.message = "abcdefghijklmnopqrst".into(); + let style = ProgressStyle::with_template("{wide_msg:>}").unwrap(); + state.message = "abcdefghijklmnopqrst".into(); style.format_state(&state, &mut buf, WIDTH); assert_eq!(&buf[0], "klmnopqrst"); buf.clear(); - let mut style = ProgressStyle::with_template("{wide_msg:^}").unwrap(); - style.message = "abcdefghijklmnopqrst".into(); + let style = ProgressStyle::with_template("{wide_msg:^}").unwrap(); + state.message = "abcdefghijklmnopqrst".into(); style.format_state(&state, &mut buf, WIDTH); assert_eq!(&buf[0], "fghijklmno"); } @@ -761,7 +757,7 @@ mod tests { let pos = Arc::new(AtomicPosition::new()); // half finished pos.set(2); - let state = ProgressState::new(Some(4), pos); + let mut state = ProgressState::new(Some(4), pos); let mut buf = Vec::new(); let style = ProgressStyle::with_template("{wide_bar}") @@ -781,8 +777,8 @@ mod tests { ); buf.clear(); - let mut style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap(); - style.message = "foobar".into(); + let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap(); + state.message = "foobar".into(); style.format_state(&state, &mut buf, WIDTH); assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m foobar \u{1b}[0m"); } From c9596f3101700d127c94a4f7317e9e383c379de6 Mon Sep 17 00:00:00 2001 From: Chris Laplante Date: Fri, 17 Jun 2022 18:08:36 -0400 Subject: [PATCH 2/3] Implement customizable tab expansion (#150) --- src/progress_bar.rs | 69 +++++++++++++++++-------------------- src/state.rs | 83 ++++++++++++++++++++++++++++++++++++++------- src/style.rs | 67 +++++++++++++++++++++++++----------- 3 files changed, 148 insertions(+), 71 deletions(-) diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 743d560c..8c1c1335 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -5,12 +5,11 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak}; use std::time::{Duration, Instant}; use std::{fmt, io, thread}; -use console::strip_ansi_codes; #[cfg(test)] use once_cell::sync::Lazy; use crate::draw_target::ProgressDrawTarget; -use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset}; +use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset, TabExpandedString}; use crate::style::ProgressStyle; use crate::{ProgressBarIter, ProgressIterator, ProgressState}; @@ -70,15 +69,25 @@ impl ProgressBar { self } + /// A convenience builder-like function for a progress bar with a given tab width + pub fn with_tab_width(self, tab_width: usize) -> ProgressBar { + self.state().set_tab_width(tab_width); + self + } + /// A convenience builder-like function for a progress bar with a given prefix pub fn with_prefix(self, prefix: impl Into>) -> ProgressBar { - self.state().state.prefix = prefix.into(); + let mut state = self.state(); + state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width); + drop(state); self } /// A convenience builder-like function for a progress bar with a given message pub fn with_message(self, message: impl Into>) -> ProgressBar { - self.state().state.message = message.into(); + let mut state = self.state(); + state.state.message = TabExpandedString::new(message.into(), state.tab_width); + drop(state); self } @@ -123,7 +132,14 @@ impl ProgressBar { /// /// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it. pub fn set_style(&self, style: ProgressStyle) { - self.state().style = style; + self.state().set_style(style); + } + + /// Sets the tab width (default: 8). All tabs will be expanded to this many spaces. + pub fn set_tab_width(&mut self, tab_width: usize) { + let mut state = self.state(); + state.set_tab_width(tab_width); + state.draw(true, Instant::now()).unwrap(); } /// Spawns a background thread to tick the progress bar @@ -244,7 +260,9 @@ impl ProgressBar { /// For the prefix to be visible, the `{prefix}` placeholder must be present in the template /// (see [`ProgressStyle`]). pub fn set_prefix(&self, prefix: impl Into>) { - self.state().set_prefix(Instant::now(), prefix.into()); + let mut state = self.state(); + state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width); + state.update_estimate_and_draw(Instant::now()); } /// Sets the current message of the progress bar @@ -252,7 +270,9 @@ impl ProgressBar { /// For the message to be visible, the `{msg}` placeholder must be present in the template (see /// [`ProgressStyle`]). pub fn set_message(&self, msg: impl Into>) { - self.state().set_message(Instant::now(), msg.into()) + let mut state = self.state(); + state.state.message = TabExpandedString::new(msg.into(), state.tab_width); + state.update_estimate_and_draw(Instant::now()); } /// Creates a new weak reference to this `ProgressBar` @@ -516,23 +536,13 @@ impl ProgressBar { } /// Current message - pub fn message(&self) -> Cow<'static, str> { - self.state().state.message.clone() - } - - /// Current message (with ANSI escape codes stripped) - pub fn message_unstyled(&self) -> String { - strip_ansi_codes(&self.message()).to_string() + pub fn message(&self) -> String { + self.state().state.message.expanded().to_string() } /// Current prefix - pub fn prefix(&self) -> Cow<'static, str> { - self.state().state.prefix.clone() - } - - /// Current prefix (with ANSI escape codes stripped) - pub fn prefix_unstyled(&self) -> String { - strip_ansi_codes(&self.prefix()).to_string() + pub fn prefix(&self) -> String { + self.state().state.prefix.expanded().to_string() } #[inline] @@ -662,7 +672,6 @@ pub(crate) static TICKER_TEST: Lazy> = Lazy::new(Mutex::default); #[cfg(test)] mod tests { use super::*; - use console::Style; #[allow(clippy::float_cmp)] #[test] @@ -762,20 +771,4 @@ mod tests { drop(pb2); assert!(!TICKER_RUNNING.load(Ordering::SeqCst)); } - - #[test] - fn access_message_and_prefix() { - let pb = ProgressBar::new(80); - pb.set_message(Style::new().red().bold().apply_to("text").to_string()); - pb.set_prefix( - Style::new() - .on_blue() - .italic() - .apply_to("prefix!") - .to_string(), - ); - - assert_eq!(pb.message_unstyled(), "text"); - assert_eq!(pb.prefix_unstyled(), "prefix!"); - } } diff --git a/src/state.rs b/src/state.rs index b1d93531..d18ac5be 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,6 +12,7 @@ pub(crate) struct BarState { pub(crate) on_finish: ProgressFinish, pub(crate) style: ProgressStyle, pub(crate) state: ProgressState, + pub(crate) tab_width: usize, } impl BarState { @@ -25,6 +26,7 @@ impl BarState { on_finish: ProgressFinish::default(), style: ProgressStyle::default_bar(), state: ProgressState::new(len, pos), + tab_width: DEFAULT_TAB_WIDTH, } } @@ -42,7 +44,7 @@ impl BarState { if let Some(len) = self.state.len { self.state.pos.set(len); } - self.state.message = msg; + self.state.message = TabExpandedString::new(msg, self.tab_width); } ProgressFinish::AndClear => { if let Some(len) = self.state.len { @@ -51,7 +53,9 @@ impl BarState { self.state.status = Status::DoneHidden; } ProgressFinish::Abandon => {} - ProgressFinish::AbandonWithMessage(msg) => self.state.message = msg, + ProgressFinish::AbandonWithMessage(msg) => { + self.state.message = TabExpandedString::new(msg, self.tab_width) + } } // There's no need to update the estimate here; once the `status` is no longer @@ -92,14 +96,16 @@ impl BarState { self.update_estimate_and_draw(now); } - pub(crate) fn set_message(&mut self, now: Instant, msg: Cow<'static, str>) { - self.state.message = msg; - self.update_estimate_and_draw(now); + pub(crate) fn set_tab_width(&mut self, tab_width: usize) { + self.tab_width = tab_width; + self.state.message.set_tab_width(tab_width); + self.state.prefix.set_tab_width(tab_width); + self.style.set_tab_width(tab_width); } - pub(crate) fn set_prefix(&mut self, now: Instant, prefix: Cow<'static, str>) { - self.state.prefix = prefix; - self.update_estimate_and_draw(now); + pub(crate) fn set_style(&mut self, style: ProgressStyle) { + self.style = style; + self.style.set_tab_width(self.tab_width); } pub(crate) fn tick(&mut self, now: Instant) { @@ -107,7 +113,7 @@ impl BarState { self.update_estimate_and_draw(now); } - fn update_estimate_and_draw(&mut self, now: Instant) { + pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) { let pos = self.state.pos.pos.load(Ordering::Relaxed); self.state.est.record(pos, now); let _ = self.draw(false, now); @@ -190,8 +196,8 @@ pub struct ProgressState { pub(crate) started: Instant, status: Status, est: Estimator, - pub(crate) message: Cow<'static, str>, - pub(crate) prefix: Cow<'static, str>, + pub(crate) message: TabExpandedString, + pub(crate) prefix: TabExpandedString, } impl ProgressState { @@ -203,8 +209,8 @@ impl ProgressState { status: Status::InProgress, started: Instant::now(), est: Estimator::new(Instant::now()), - message: "".into(), - prefix: "".into(), + message: TabExpandedString::NoTabs("".into()), + prefix: TabExpandedString::NoTabs("".into()), } } @@ -289,6 +295,55 @@ impl ProgressState { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub(crate) enum TabExpandedString { + NoTabs(Cow<'static, str>), + WithTabs { + original: Cow<'static, str>, + expanded: String, + tab_width: usize, + }, +} + +impl TabExpandedString { + pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self { + let expanded = s.replace('\t', &" ".repeat(tab_width)); + if s == expanded { + Self::NoTabs(s) + } else { + Self::WithTabs { + original: s, + expanded, + tab_width, + } + } + } + + pub(crate) fn expanded(&self) -> &str { + match &self { + Self::NoTabs(s) => { + debug_assert!(!s.contains('\t')); + s + } + Self::WithTabs { expanded, .. } => expanded, + } + } + + pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) { + if let TabExpandedString::WithTabs { + original, + expanded, + tab_width, + } = self + { + if *tab_width != new_tab_width { + *tab_width = new_tab_width; + *expanded = original.replace('\t', &" ".repeat(new_tab_width)); + } + } + } +} + /// Estimate the number of seconds per step /// /// Ring buffer with constant capacity. Used by `ProgressBar`s to display `{eta}`, @@ -486,6 +541,8 @@ pub(crate) enum Status { DoneHidden, } +pub(crate) const DEFAULT_TAB_WIDTH: usize = 8; + #[cfg(test)] mod tests { use super::*; diff --git a/src/style.rs b/src/style.rs index ccadffc0..56cc07cb 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,4 +1,3 @@ - use std::collections::HashMap; use std::fmt::{self, Write}; use std::mem; @@ -10,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::format::{ BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration, }; -use crate::state::ProgressState; +use crate::state::{ProgressState, TabExpandedString, DEFAULT_TAB_WIDTH}; /// Controls the rendering style of progress bars #[derive(Clone)] @@ -21,6 +20,7 @@ pub struct ProgressStyle { // how unicode-big each char in progress_chars is char_width: usize, format_map: HashMap<&'static str, fn(&ProgressState) -> String>, + tab_width: usize, } #[cfg(feature = "unicode-segmentation")] @@ -75,6 +75,11 @@ impl ProgressStyle { 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); @@ -87,6 +92,7 @@ impl ProgressStyle { char_width, template, format_map: HashMap::default(), + tab_width: DEFAULT_TAB_WIDTH, } } @@ -232,7 +238,7 @@ impl ProgressStyle { } => { buf.clear(); if let Some(formatter) = self.format_map.get(key.as_str()) { - buf.push_str(&formatter(state)); + buf.push_str(&formatter(state).replace('\t', &" ".repeat(self.tab_width))); } else { match key.as_str() { "wide_bar" => { @@ -254,8 +260,8 @@ impl ProgressStyle { wide = Some(WideElement::Message { align }); buf.push('\x00'); } - "msg" => buf.push_str(&state.message), - "prefix" => buf.push_str(&state.prefix), + "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() @@ -338,7 +344,7 @@ impl ProgressStyle { }, } } - TemplatePart::Literal(s) => cur.push_str(s), + TemplatePart::Literal(s) => cur.push_str(s.expanded()), TemplatePart::NewLine => lines.push(match wide { Some(inner) => { inner.expand(mem::take(&mut cur), self, state, &mut buf, target_width) @@ -388,7 +394,7 @@ impl<'a> WideElement<'a> { buf.write_fmt(format_args!( "{}", PaddedStringDisplay { - str: &state.message, + str: state.message.expanded(), width: left, align: *align, truncate: true, @@ -413,7 +419,7 @@ struct Template { } impl Template { - fn from_str(s: &str) -> Result { + fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result { use State::*; let (mut state, mut parts, mut buf) = (Literal, vec![], String::new()); for c in s.chars() { @@ -421,7 +427,10 @@ impl Template { (Literal, '{') => (MaybeOpen, None), (Literal, '\n') => { if !buf.is_empty() { - parts.push(TemplatePart::Literal(mem::take(&mut buf))); + parts.push(TemplatePart::Literal(TabExpandedString::new( + mem::take(&mut buf).into(), + tab_width, + ))); } parts.push(TemplatePart::NewLine); (Literal, None) @@ -437,7 +446,10 @@ impl Template { let mut new = String::from("{"); new.push_str(&buf); buf.clear(); - parts.push(TemplatePart::Literal(new)); + parts.push(TemplatePart::Literal(TabExpandedString::new( + new.into(), + tab_width, + ))); (Literal, None) } (MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)), @@ -488,9 +500,9 @@ impl Template { }; match (state, new.0) { - (MaybeOpen, Key) if !buf.is_empty() => { - parts.push(TemplatePart::Literal(mem::take(&mut buf))) - } + (MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal( + TabExpandedString::new(mem::take(&mut buf).into(), tab_width), + )), (Key, Align) | (Key, Literal) if !buf.is_empty() => { parts.push(TemplatePart::Placeholder { key: mem::take(&mut buf), @@ -529,11 +541,26 @@ impl Template { } if matches!(state, Literal | DoubleClose) && !buf.is_empty() { - parts.push(TemplatePart::Literal(buf)); + parts.push(TemplatePart::Literal(TabExpandedString::new( + buf.into(), + tab_width, + ))); } Ok(Self { parts }) } + + fn from_str(s: &str) -> Result { + Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH) + } + + fn set_tab_width(&mut self, new_tab_width: usize) { + for part in self.parts.iter_mut() { + if let TemplatePart::Literal(s) = part { + s.set_tab_width(new_tab_width) + } + } + } } #[derive(Debug)] @@ -556,7 +583,7 @@ impl std::error::Error for TemplateError {} #[derive(Clone, Debug, PartialEq, Eq)] enum TemplatePart { - Literal(String), + Literal(TabExpandedString), Placeholder { key: String, align: Alignment, @@ -669,7 +696,7 @@ mod tests { use std::sync::Arc; use super::*; - use crate::state::{AtomicPosition, ProgressState}; + use crate::state::{AtomicPosition, ProgressState, TabExpandedString}; #[test] fn test_expand_template() { @@ -733,19 +760,19 @@ mod tests { let mut buf = Vec::new(); let style = ProgressStyle::with_template("{wide_msg}").unwrap(); - state.message = "abcdefghijklmnopqrst".into(); + 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 = "abcdefghijklmnopqrst".into(); + 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 = "abcdefghijklmnopqrst".into(); + state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into()); style.format_state(&state, &mut buf, WIDTH); assert_eq!(&buf[0], "fghijklmno"); } @@ -778,7 +805,7 @@ mod tests { buf.clear(); let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap(); - state.message = "foobar".into(); + 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"); } From b4fa333c56a6c8276474be3760565e21f61ea411 Mon Sep 17 00:00:00 2001 From: Chris Laplante Date: Thu, 23 Jun 2022 17:57:32 -0400 Subject: [PATCH 3/3] add render tests for tab handling --- tests/render.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/render.rs b/tests/render.rs index 45416a5b..e696519d 100644 --- a/tests/render.rs +++ b/tests/render.rs @@ -534,3 +534,63 @@ don't erase me either assert_eq!(in_mem.contents(), "don't erase me either"); } + +#[test] +fn basic_tab_expansion() { + let in_mem = InMemoryTerm::new(10, 80); + let mp = + MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); + + let mut spinner = mp.add(ProgressBar::new_spinner().with_message("Test\t:)")); + spinner.tick(); + + // 8 is the default number of spaces + assert_eq!(in_mem.contents(), "⠁ Test :)"); + + spinner.set_tab_width(4); + assert_eq!(in_mem.contents(), "⠁ Test :)"); +} + +#[test] +fn tab_expansion_in_template() { + let in_mem = InMemoryTerm::new(10, 80); + let mp = + MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); + + let mut spinner = mp.add( + ProgressBar::new_spinner() + .with_message("Test\t:)") + .with_prefix("Pre\tfix!") + .with_style(ProgressStyle::with_template("{spinner}{prefix}\t{msg}").unwrap()), + ); + + spinner.tick(); + assert_eq!(in_mem.contents(), "⠁Pre fix! Test :)"); + + spinner.set_tab_width(4); + assert_eq!(in_mem.contents(), "⠁Pre fix! Test :)"); + + spinner.set_tab_width(2); + assert_eq!(in_mem.contents(), "⠁Pre fix! Test :)"); +} + +#[test] +fn progress_style_tab_width_unification() { + let in_mem = InMemoryTerm::new(10, 80); + let mp = + MultiProgress::with_draw_target(ProgressDrawTarget::term_like(Box::new(in_mem.clone()))); + + // Style will have default of 8 spaces for tabs + let style = ProgressStyle::with_template("{msg}\t{msg}").unwrap(); + + let spinner = mp.add( + ProgressBar::new_spinner() + .with_message("OK") + .with_tab_width(4), + ); + + // Setting the spinner's style to |style| should override the style's tab width with that of bar + spinner.set_style(style); + spinner.tick(); + assert_eq!(in_mem.contents(), "OK OK"); +}