Skip to content

Commit 70ebceb

Browse files
Implement customizable tab expansion (console-rs#150)
1 parent 72c25c1 commit 70ebceb

File tree

3 files changed

+148
-71
lines changed

3 files changed

+148
-71
lines changed

src/progress_bar.rs

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
55
use std::time::{Duration, Instant};
66
use std::{fmt, io, thread};
77

8-
use console::strip_ansi_codes;
98
#[cfg(test)]
109
use once_cell::sync::Lazy;
1110

1211
use crate::draw_target::ProgressDrawTarget;
13-
use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset};
12+
use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset, TabExpandedString};
1413
use crate::style::ProgressStyle;
1514
use crate::{ProgressBarIter, ProgressIterator, ProgressState};
1615

@@ -70,15 +69,25 @@ impl ProgressBar {
7069
self
7170
}
7271

72+
/// A convenience builder-like function for a progress bar with a given tab width
73+
pub fn with_tab_width(self, tab_width: usize) -> ProgressBar {
74+
self.state().set_tab_width(tab_width);
75+
self
76+
}
77+
7378
/// A convenience builder-like function for a progress bar with a given prefix
7479
pub fn with_prefix(self, prefix: impl Into<Cow<'static, str>>) -> ProgressBar {
75-
self.state().state.prefix = prefix.into();
80+
let mut state = self.state();
81+
state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width);
82+
drop(state);
7683
self
7784
}
7885

7986
/// A convenience builder-like function for a progress bar with a given message
8087
pub fn with_message(self, message: impl Into<Cow<'static, str>>) -> ProgressBar {
81-
self.state().state.message = message.into();
88+
let mut state = self.state();
89+
state.state.message = TabExpandedString::new(message.into(), state.tab_width);
90+
drop(state);
8291
self
8392
}
8493

@@ -123,7 +132,14 @@ impl ProgressBar {
123132
///
124133
/// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it.
125134
pub fn set_style(&self, style: ProgressStyle) {
126-
self.state().style = style;
135+
self.state().set_style(style);
136+
}
137+
138+
/// Sets the tab width (default: 8). All tabs will be expanded to this many spaces.
139+
pub fn set_tab_width(&mut self, tab_width: usize) {
140+
let mut state = self.state();
141+
state.set_tab_width(tab_width);
142+
state.draw(true, Instant::now()).unwrap();
127143
}
128144

129145
/// Spawns a background thread to tick the progress bar
@@ -244,15 +260,19 @@ impl ProgressBar {
244260
/// For the prefix to be visible, the `{prefix}` placeholder must be present in the template
245261
/// (see [`ProgressStyle`]).
246262
pub fn set_prefix(&self, prefix: impl Into<Cow<'static, str>>) {
247-
self.state().set_prefix(Instant::now(), prefix.into());
263+
let mut state = self.state();
264+
state.state.prefix = TabExpandedString::new(prefix.into(), state.tab_width);
265+
state.update_estimate_and_draw(Instant::now());
248266
}
249267

250268
/// Sets the current message of the progress bar
251269
///
252270
/// For the message to be visible, the `{msg}` placeholder must be present in the template (see
253271
/// [`ProgressStyle`]).
254272
pub fn set_message(&self, msg: impl Into<Cow<'static, str>>) {
255-
self.state().set_message(Instant::now(), msg.into())
273+
let mut state = self.state();
274+
state.state.message = TabExpandedString::new(msg.into(), state.tab_width);
275+
state.update_estimate_and_draw(Instant::now());
256276
}
257277

258278
/// Creates a new weak reference to this `ProgressBar`
@@ -516,23 +536,13 @@ impl ProgressBar {
516536
}
517537

518538
/// Current message
519-
pub fn message(&self) -> Cow<'static, str> {
520-
self.state().state.message.clone()
521-
}
522-
523-
/// Current message (with ANSI escape codes stripped)
524-
pub fn message_unstyled(&self) -> String {
525-
strip_ansi_codes(&self.message()).to_string()
539+
pub fn message(&self) -> String {
540+
self.state().state.message.expanded().to_string()
526541
}
527542

528543
/// Current prefix
529-
pub fn prefix(&self) -> Cow<'static, str> {
530-
self.state().state.prefix.clone()
531-
}
532-
533-
/// Current prefix (with ANSI escape codes stripped)
534-
pub fn prefix_unstyled(&self) -> String {
535-
strip_ansi_codes(&self.prefix()).to_string()
544+
pub fn prefix(&self) -> String {
545+
self.state().state.prefix.expanded().to_string()
536546
}
537547

538548
#[inline]
@@ -662,7 +672,6 @@ pub(crate) static TICKER_TEST: Lazy<Mutex<()>> = Lazy::new(Mutex::default);
662672
#[cfg(test)]
663673
mod tests {
664674
use super::*;
665-
use console::Style;
666675

667676
#[allow(clippy::float_cmp)]
668677
#[test]
@@ -762,20 +771,4 @@ mod tests {
762771
drop(pb2);
763772
assert!(!TICKER_RUNNING.load(Ordering::SeqCst));
764773
}
765-
766-
#[test]
767-
fn access_message_and_prefix() {
768-
let pb = ProgressBar::new(80);
769-
pb.set_message(Style::new().red().bold().apply_to("text").to_string());
770-
pb.set_prefix(
771-
Style::new()
772-
.on_blue()
773-
.italic()
774-
.apply_to("prefix!")
775-
.to_string(),
776-
);
777-
778-
assert_eq!(pb.message_unstyled(), "text");
779-
assert_eq!(pb.prefix_unstyled(), "prefix!");
780-
}
781774
}

src/state.rs

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(crate) struct BarState {
1212
pub(crate) on_finish: ProgressFinish,
1313
pub(crate) style: ProgressStyle,
1414
pub(crate) state: ProgressState,
15+
pub(crate) tab_width: usize,
1516
}
1617

1718
impl BarState {
@@ -25,6 +26,7 @@ impl BarState {
2526
on_finish: ProgressFinish::default(),
2627
style: ProgressStyle::default_bar(),
2728
state: ProgressState::new(len, pos),
29+
tab_width: DEFAULT_TAB_WIDTH,
2830
}
2931
}
3032

@@ -42,7 +44,7 @@ impl BarState {
4244
if let Some(len) = self.state.len {
4345
self.state.pos.set(len);
4446
}
45-
self.state.message = msg;
47+
self.state.message = TabExpandedString::new(msg, self.tab_width);
4648
}
4749
ProgressFinish::AndClear => {
4850
if let Some(len) = self.state.len {
@@ -51,7 +53,9 @@ impl BarState {
5153
self.state.status = Status::DoneHidden;
5254
}
5355
ProgressFinish::Abandon => {}
54-
ProgressFinish::AbandonWithMessage(msg) => self.state.message = msg,
56+
ProgressFinish::AbandonWithMessage(msg) => {
57+
self.state.message = TabExpandedString::new(msg, self.tab_width)
58+
}
5559
}
5660

5761
// There's no need to update the estimate here; once the `status` is no longer
@@ -92,22 +96,24 @@ impl BarState {
9296
self.update_estimate_and_draw(now);
9397
}
9498

95-
pub(crate) fn set_message(&mut self, now: Instant, msg: Cow<'static, str>) {
96-
self.state.message = msg;
97-
self.update_estimate_and_draw(now);
99+
pub(crate) fn set_tab_width(&mut self, tab_width: usize) {
100+
self.tab_width = tab_width;
101+
self.state.message.change_tab_width(tab_width);
102+
self.state.prefix.change_tab_width(tab_width);
103+
self.style.change_tab_width(tab_width);
98104
}
99105

100-
pub(crate) fn set_prefix(&mut self, now: Instant, prefix: Cow<'static, str>) {
101-
self.state.prefix = prefix;
102-
self.update_estimate_and_draw(now);
106+
pub(crate) fn set_style(&mut self, style: ProgressStyle) {
107+
self.style = style;
108+
self.style.change_tab_width(self.tab_width);
103109
}
104110

105111
pub(crate) fn tick(&mut self, now: Instant) {
106112
self.state.tick = self.state.tick.saturating_add(1);
107113
self.update_estimate_and_draw(now);
108114
}
109115

110-
fn update_estimate_and_draw(&mut self, now: Instant) {
116+
pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) {
111117
let pos = self.state.pos.pos.load(Ordering::Relaxed);
112118
self.state.est.record(pos, now);
113119
let _ = self.draw(false, now);
@@ -190,8 +196,8 @@ pub struct ProgressState {
190196
pub(crate) started: Instant,
191197
status: Status,
192198
est: Estimator,
193-
pub(crate) message: Cow<'static, str>,
194-
pub(crate) prefix: Cow<'static, str>,
199+
pub(crate) message: TabExpandedString,
200+
pub(crate) prefix: TabExpandedString,
195201
}
196202

197203
impl ProgressState {
@@ -203,8 +209,8 @@ impl ProgressState {
203209
status: Status::InProgress,
204210
started: Instant::now(),
205211
est: Estimator::new(Instant::now()),
206-
message: "".into(),
207-
prefix: "".into(),
212+
message: TabExpandedString::NoTabs("".into()),
213+
prefix: TabExpandedString::NoTabs("".into()),
208214
}
209215
}
210216

@@ -289,6 +295,55 @@ impl ProgressState {
289295
}
290296
}
291297

298+
#[derive(Debug, PartialEq, Eq, Clone)]
299+
pub(crate) enum TabExpandedString {
300+
NoTabs(Cow<'static, str>),
301+
WithTabs {
302+
original: Cow<'static, str>,
303+
expanded: String,
304+
tab_width: usize,
305+
},
306+
}
307+
308+
impl TabExpandedString {
309+
pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self {
310+
let expanded = s.replace('\t', &" ".repeat(tab_width));
311+
if s == expanded {
312+
Self::NoTabs(s)
313+
} else {
314+
Self::WithTabs {
315+
original: s,
316+
expanded,
317+
tab_width,
318+
}
319+
}
320+
}
321+
322+
pub(crate) fn expanded(&self) -> &str {
323+
match &self {
324+
Self::NoTabs(s) => {
325+
debug_assert!(!s.contains('\t'));
326+
s
327+
}
328+
Self::WithTabs { expanded, .. } => expanded,
329+
}
330+
}
331+
332+
pub(crate) fn change_tab_width(&mut self, new_tab_width: usize) {
333+
if let TabExpandedString::WithTabs {
334+
original,
335+
expanded,
336+
tab_width,
337+
} = self
338+
{
339+
if *tab_width != new_tab_width {
340+
*tab_width = new_tab_width;
341+
*expanded = original.replace('\t', &" ".repeat(new_tab_width));
342+
}
343+
}
344+
}
345+
}
346+
292347
/// Estimate the number of seconds per step
293348
///
294349
/// Ring buffer with constant capacity. Used by `ProgressBar`s to display `{eta}`,
@@ -486,6 +541,8 @@ pub(crate) enum Status {
486541
DoneHidden,
487542
}
488543

544+
pub(crate) const DEFAULT_TAB_WIDTH: usize = 8;
545+
489546
#[cfg(test)]
490547
mod tests {
491548
use super::*;

0 commit comments

Comments
 (0)