From c9596f3101700d127c94a4f7317e9e383c379de6 Mon Sep 17 00:00:00 2001 From: Chris Laplante Date: Fri, 17 Jun 2022 18:08:36 -0400 Subject: [PATCH] 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"); }