From 8bbf5a1579db5b83be351f65c007b6624cf07c74 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 | 25 +++++++--- src/state.rs | 114 ++++++++++++++++++++++++++++++++++++++++---- src/style.rs | 67 ++++++++++++++++++-------- 3 files changed, 171 insertions(+), 35 deletions(-) diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 743d560c..e5eac6e6 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -70,15 +70,21 @@ 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_without_draw(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(); + self.state().set_prefix_without_draw(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().state.message = message.into(); + self.state().set_message_without_draw(message.into()); self } @@ -123,7 +129,12 @@ 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) { + self.state().set_tab_width(Instant::now(), tab_width); } /// Spawns a background thread to tick the progress bar @@ -516,8 +527,8 @@ impl ProgressBar { } /// Current message - pub fn message(&self) -> Cow<'static, str> { - self.state().state.message.clone() + pub fn message(&self) -> String { + self.state().state.message().to_string() } /// Current message (with ANSI escape codes stripped) @@ -526,8 +537,8 @@ impl ProgressBar { } /// Current prefix - pub fn prefix(&self) -> Cow<'static, str> { - self.state().state.prefix.clone() + pub fn prefix(&self) -> String { + self.state().state.prefix().to_string() } /// Current prefix (with ANSI escape codes stripped) diff --git a/src/state.rs b/src/state.rs index b1d93531..fc200d94 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,11 +7,14 @@ use std::{fmt, io}; use crate::draw_target::ProgressDrawTarget; use crate::style::ProgressStyle; +pub(crate) const DEFAULT_TAB_WIDTH: usize = 8; + pub(crate) struct BarState { pub(crate) draw_target: ProgressDrawTarget, pub(crate) on_finish: ProgressFinish, pub(crate) style: ProgressStyle, pub(crate) state: ProgressState, + tab_width: usize, } impl BarState { @@ -25,6 +28,7 @@ impl BarState { on_finish: ProgressFinish::default(), style: ProgressStyle::default_bar(), state: ProgressState::new(len, pos), + tab_width: DEFAULT_TAB_WIDTH, } } @@ -42,7 +46,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 +55,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 @@ -93,15 +99,42 @@ impl BarState { } pub(crate) fn set_message(&mut self, now: Instant, msg: Cow<'static, str>) { - self.state.message = msg; + self.state.message = TabExpandedString::new(msg, self.tab_width); self.update_estimate_and_draw(now); } + // Called in builder context + pub(crate) fn set_message_without_draw(&mut self, msg: Cow<'static, str>) { + self.state.message = TabExpandedString::new(msg, self.tab_width); + } + pub(crate) fn set_prefix(&mut self, now: Instant, prefix: Cow<'static, str>) { - self.state.prefix = prefix; + self.state.prefix = TabExpandedString::new(prefix, self.tab_width); + self.update_estimate_and_draw(now); + } + + // Called in builder context + pub(crate) fn set_prefix_without_draw(&mut self, prefix: Cow<'static, str>) { + self.state.prefix = TabExpandedString::new(prefix, self.tab_width); + } + + pub(crate) fn set_tab_width(&mut self, now: Instant, tab_width: usize) { + self.set_tab_width_without_draw(tab_width); self.update_estimate_and_draw(now); } + pub(crate) fn set_tab_width_without_draw(&mut self, tab_width: usize) { + self.tab_width = tab_width; + self.state.message.change_tab_width(tab_width); + self.state.prefix.change_tab_width(tab_width); + self.style.change_tab_width(tab_width); + } + + pub(crate) fn set_style(&mut self, style: ProgressStyle) { + self.style = style; + self.style.change_tab_width(self.tab_width); + } + pub(crate) fn tick(&mut self, now: Instant) { self.state.tick = self.state.tick.saturating_add(1); self.update_estimate_and_draw(now); @@ -190,8 +223,8 @@ pub struct ProgressState { pub(crate) started: Instant, status: Status, est: Estimator, - pub(crate) message: Cow<'static, str>, - pub(crate) prefix: Cow<'static, str>, + message: TabExpandedString, + prefix: TabExpandedString, } impl ProgressState { @@ -203,8 +236,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()), } } @@ -287,6 +320,71 @@ impl ProgressState { pub fn set_len(&mut self, len: u64) { self.len = Some(len); } + + pub fn set_message(&mut self, msg: TabExpandedString) { + self.message = msg; + } + + pub fn message(&self) -> &str { + self.message.expanded() + } + + pub fn set_prefix(&mut self, prefix: TabExpandedString) { + self.prefix = prefix; + } + + pub fn prefix(&self) -> &str { + self.prefix.expanded() + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub 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 change_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 diff --git a/src/style.rs b/src/style.rs index ccadffc0..d4a85452 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 change_tab_width(&mut self, new_tab_width: usize) { + self.tab_width = new_tab_width; + self.template.change_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()), + "prefix" => buf.push_str(state.prefix()), "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(), 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 change_tab_width(&mut self, new_tab_width: usize) { + for part in self.parts.iter_mut() { + if let TemplatePart::Literal(s) = part { + s.change_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.set_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.set_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.set_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.set_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"); }