Skip to content

Commit 21daba5

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

File tree

3 files changed

+177
-35
lines changed

3 files changed

+177
-35
lines changed

src/progress_bar.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,21 @@ impl ProgressBar {
7070
self
7171
}
7272

73+
/// A convenience builder-like function for a progress bar with a given tab width
74+
pub fn with_tab_width(self, tab_width: usize) -> ProgressBar {
75+
self.state().set_tab_width_without_draw(tab_width);
76+
self
77+
}
78+
7379
/// A convenience builder-like function for a progress bar with a given prefix
7480
pub fn with_prefix(self, prefix: impl Into<Cow<'static, str>>) -> ProgressBar {
75-
self.state().state.prefix = prefix.into();
81+
self.state().set_prefix_without_draw(prefix.into());
7682
self
7783
}
7884

7985
/// A convenience builder-like function for a progress bar with a given message
8086
pub fn with_message(self, message: impl Into<Cow<'static, str>>) -> ProgressBar {
81-
self.state().state.message = message.into();
87+
self.state().set_message_without_draw(message.into());
8288
self
8389
}
8490

@@ -123,7 +129,12 @@ impl ProgressBar {
123129
///
124130
/// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it.
125131
pub fn set_style(&self, style: ProgressStyle) {
126-
self.state().style = style;
132+
self.state().set_style(style);
133+
}
134+
135+
/// Sets the tab width (default: 8). All tabs will be expanded to this many spaces.
136+
pub fn set_tab_width(&mut self, tab_width: usize) {
137+
self.state().set_tab_width(Instant::now(), tab_width);
127138
}
128139

129140
/// Spawns a background thread to tick the progress bar
@@ -516,8 +527,8 @@ impl ProgressBar {
516527
}
517528

518529
/// Current message
519-
pub fn message(&self) -> Cow<'static, str> {
520-
self.state().state.message.clone()
530+
pub fn message(&self) -> String {
531+
self.state().state.message().to_string()
521532
}
522533

523534
/// Current message (with ANSI escape codes stripped)
@@ -526,8 +537,8 @@ impl ProgressBar {
526537
}
527538

528539
/// Current prefix
529-
pub fn prefix(&self) -> Cow<'static, str> {
530-
self.state().state.prefix.clone()
540+
pub fn prefix(&self) -> String {
541+
self.state().state.prefix().to_string()
531542
}
532543

533544
/// Current prefix (with ANSI escape codes stripped)

src/state.rs

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ use std::{fmt, io};
77
use crate::draw_target::ProgressDrawTarget;
88
use crate::style::ProgressStyle;
99

10+
pub(crate) const DEFAULT_TAB_WIDTH: usize = 8;
11+
1012
pub(crate) struct BarState {
1113
pub(crate) draw_target: ProgressDrawTarget,
1214
pub(crate) on_finish: ProgressFinish,
1315
pub(crate) style: ProgressStyle,
1416
pub(crate) state: ProgressState,
17+
tab_width: usize,
1518
}
1619

1720
impl BarState {
@@ -25,6 +28,7 @@ impl BarState {
2528
on_finish: ProgressFinish::default(),
2629
style: ProgressStyle::default_bar(),
2730
state: ProgressState::new(len, pos),
31+
tab_width: DEFAULT_TAB_WIDTH,
2832
}
2933
}
3034

@@ -42,7 +46,7 @@ impl BarState {
4246
if let Some(len) = self.state.len {
4347
self.state.pos.set(len);
4448
}
45-
self.state.message = msg;
49+
self.state.message = TabExpandedString::new(msg, self.tab_width);
4650
}
4751
ProgressFinish::AndClear => {
4852
if let Some(len) = self.state.len {
@@ -51,7 +55,9 @@ impl BarState {
5155
self.state.status = Status::DoneHidden;
5256
}
5357
ProgressFinish::Abandon => {}
54-
ProgressFinish::AbandonWithMessage(msg) => self.state.message = msg,
58+
ProgressFinish::AbandonWithMessage(msg) => {
59+
self.state.message = TabExpandedString::new(msg, self.tab_width)
60+
}
5561
}
5662

5763
// There's no need to update the estimate here; once the `status` is no longer
@@ -93,15 +99,42 @@ impl BarState {
9399
}
94100

95101
pub(crate) fn set_message(&mut self, now: Instant, msg: Cow<'static, str>) {
96-
self.state.message = msg;
102+
self.state.message = TabExpandedString::new(msg, self.tab_width);
97103
self.update_estimate_and_draw(now);
98104
}
99105

106+
// Called in builder context
107+
pub(crate) fn set_message_without_draw(&mut self, msg: Cow<'static, str>) {
108+
self.state.message = TabExpandedString::new(msg, self.tab_width);
109+
}
110+
100111
pub(crate) fn set_prefix(&mut self, now: Instant, prefix: Cow<'static, str>) {
101-
self.state.prefix = prefix;
112+
self.state.prefix = TabExpandedString::new(prefix, self.tab_width);
113+
self.update_estimate_and_draw(now);
114+
}
115+
116+
// Called in builder context
117+
pub(crate) fn set_prefix_without_draw(&mut self, prefix: Cow<'static, str>) {
118+
self.state.prefix = TabExpandedString::new(prefix, self.tab_width);
119+
}
120+
121+
pub(crate) fn set_tab_width(&mut self, now: Instant, tab_width: usize) {
122+
self.set_tab_width_without_draw(tab_width);
102123
self.update_estimate_and_draw(now);
103124
}
104125

126+
pub(crate) fn set_tab_width_without_draw(&mut self, tab_width: usize) {
127+
self.tab_width = tab_width;
128+
self.state.message.change_tab_width(tab_width);
129+
self.state.prefix.change_tab_width(tab_width);
130+
self.style.change_tab_width(tab_width);
131+
}
132+
133+
pub(crate) fn set_style(&mut self, style: ProgressStyle) {
134+
self.style = style;
135+
self.style.change_tab_width(self.tab_width);
136+
}
137+
105138
pub(crate) fn tick(&mut self, now: Instant) {
106139
self.state.tick = self.state.tick.saturating_add(1);
107140
self.update_estimate_and_draw(now);
@@ -190,8 +223,8 @@ pub struct ProgressState {
190223
pub(crate) started: Instant,
191224
status: Status,
192225
est: Estimator,
193-
pub(crate) message: Cow<'static, str>,
194-
pub(crate) prefix: Cow<'static, str>,
226+
message: TabExpandedString,
227+
prefix: TabExpandedString,
195228
}
196229

197230
impl ProgressState {
@@ -203,8 +236,8 @@ impl ProgressState {
203236
status: Status::InProgress,
204237
started: Instant::now(),
205238
est: Estimator::new(Instant::now()),
206-
message: "".into(),
207-
prefix: "".into(),
239+
message: TabExpandedString::NoTabs("".into()),
240+
prefix: TabExpandedString::NoTabs("".into()),
208241
}
209242
}
210243

@@ -287,6 +320,71 @@ impl ProgressState {
287320
pub fn set_len(&mut self, len: u64) {
288321
self.len = Some(len);
289322
}
323+
324+
pub fn set_message(&mut self, msg: TabExpandedString) {
325+
self.message = msg;
326+
}
327+
328+
pub fn message(&self) -> &str {
329+
self.message.expanded()
330+
}
331+
332+
pub fn set_prefix(&mut self, prefix: TabExpandedString) {
333+
self.prefix = prefix;
334+
}
335+
336+
pub fn prefix(&self) -> &str {
337+
self.prefix.expanded()
338+
}
339+
}
340+
341+
#[derive(Debug, PartialEq, Eq, Clone)]
342+
pub enum TabExpandedString {
343+
NoTabs(Cow<'static, str>),
344+
WithTabs {
345+
original: Cow<'static, str>,
346+
expanded: String,
347+
tab_width: usize,
348+
},
349+
}
350+
351+
impl TabExpandedString {
352+
pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self {
353+
let expanded = s.replace('\t', &" ".repeat(tab_width));
354+
if s == expanded {
355+
Self::NoTabs(s)
356+
} else {
357+
Self::WithTabs {
358+
original: s,
359+
expanded,
360+
tab_width,
361+
}
362+
}
363+
}
364+
365+
pub(crate) fn expanded(&self) -> &str {
366+
match &self {
367+
Self::NoTabs(s) => {
368+
debug_assert!(!s.contains('\t'));
369+
s
370+
}
371+
Self::WithTabs { expanded, .. } => expanded,
372+
}
373+
}
374+
375+
pub(crate) fn change_tab_width(&mut self, new_tab_width: usize) {
376+
if let TabExpandedString::WithTabs {
377+
original,
378+
expanded,
379+
tab_width,
380+
} = self
381+
{
382+
if *tab_width != new_tab_width {
383+
*tab_width = new_tab_width;
384+
*expanded = original.replace('\t', &" ".repeat(new_tab_width));
385+
}
386+
}
387+
}
290388
}
291389

292390
/// Estimate the number of seconds per step

0 commit comments

Comments
 (0)