blob: 3b1176abfbf958b22d5c4649f28875cc4f58f3d3 [file] [log] [blame]
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::mem;
use std::time::Instant;
use console::{measure_text_width, Style};
#[cfg(feature = "unicode-segmentation")]
use unicode_segmentation::UnicodeSegmentation;
use crate::format::{
BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
};
use crate::state::{ProgressState, TabExpandedString, DEFAULT_TAB_WIDTH};
#[derive(Clone)]
pub struct ProgressStyle {
tick_strings: Vec<Box<str>>,
progress_chars: Vec<Box<str>>,
template: Template,
// how unicode-big each char in progress_chars is
char_width: usize,
tab_width: usize,
pub(crate) format_map: HashMap<&'static str, Box<dyn ProgressTracker>>,
}
#[cfg(feature = "unicode-segmentation")]
fn segment(s: &str) -> Vec<Box<str>> {
UnicodeSegmentation::graphemes(s, true)
.map(|s| s.into())
.collect()
}
#[cfg(not(feature = "unicode-segmentation"))]
fn segment(s: &str) -> Vec<Box<str>> {
s.chars().map(|x| x.to_string().into()).collect()
}
#[cfg(feature = "unicode_width")]
fn measure(s: &str) -> usize {
unicode_width::UnicodeWidthStr::width(s)
}
#[cfg(not(feature = "unicode_width"))]
fn measure(s: &str) -> usize {
s.chars().count()
}
/// finds the unicode-aware width of the passed grapheme cluters
/// panics on an empty parameter, or if the characters are not equal-width
fn width(c: &[Box<str>]) -> usize {
c.iter()
.map(|s| measure(s.as_ref()))
.fold(None, |acc, new| {
match acc {
None => return Some(new),
Some(old) => assert_eq!(old, new, "got passed un-equal width progress characters"),
}
acc
})
.unwrap()
}
impl ProgressStyle {
/// Returns the default progress bar style for bars
pub fn default_bar() -> ProgressStyle {
Self::new(Template::from_str("{wide_bar} {pos}/{len}").unwrap())
}
/// Returns the default progress bar style for spinners
pub fn default_spinner() -> Self {
Self::new(Template::from_str("{spinner} {msg}").unwrap())
}
pub fn with_template(template: &str) -> Result<Self, TemplateError> {
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);
Self {
tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ "
.chars()
.map(|c| c.to_string().into())
.collect(),
progress_chars,
char_width,
template,
format_map: HashMap::default(),
tab_width: DEFAULT_TAB_WIDTH,
}
}
/// Sets the tick character sequence for spinners
pub fn tick_chars(mut self, s: &str) -> ProgressStyle {
self.tick_strings = s.chars().map(|c| c.to_string().into()).collect();
// Format bar will panic with some potentially confusing message, better to panic here
// with a message explicitly informing of the problem
assert!(
self.tick_strings.len() >= 2,
"at least 2 tick chars required"
);
self
}
/// Sets the tick string sequence for spinners
pub fn tick_strings(mut self, s: &[&str]) -> ProgressStyle {
self.tick_strings = s.iter().map(|s| s.to_string().into()).collect();
// Format bar will panic with some potentially confusing message, better to panic here
// with a message explicitly informing of the problem
assert!(
self.progress_chars.len() >= 2,
"at least 2 tick strings required"
);
self
}
/// Sets the progress characters `(filled, current, to do)`
///
/// You can pass more than three for a more detailed display.
/// All passed grapheme clusters need to be of equal width.
pub fn progress_chars(mut self, s: &str) -> ProgressStyle {
self.progress_chars = segment(s);
// Format bar will panic with some potentially confusing message, better to panic here
// with a message explicitly informing of the problem
assert!(
self.progress_chars.len() >= 2,
"at least 2 progress chars required"
);
self.char_width = width(&self.progress_chars);
self
}
/// Adds a custom key that owns a [`ProgressTracker`] to the template
pub fn with_key<S: ProgressTracker + 'static>(
mut self,
key: &'static str,
f: S,
) -> ProgressStyle {
self.format_map.insert(key, Box::new(f));
self
}
/// Sets the template string for the progress bar
///
/// Review the [list of template keys](./index.html#templates) for more information.
pub fn template(mut self, s: &str) -> Result<ProgressStyle, TemplateError> {
self.template = Template::from_str(s)?;
Ok(self)
}
fn current_tick_str(&self, state: &ProgressState) -> &str {
match state.is_finished() {
true => self.get_final_tick_str(),
false => self.get_tick_str(state.tick),
}
}
/// Returns the tick string for a given number
pub fn get_tick_str(&self, idx: u64) -> &str {
&self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)]
}
/// Returns the tick string for the finished state
pub fn get_final_tick_str(&self) -> &str {
&self.tick_strings[self.tick_strings.len() - 1]
}
fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> BarDisplay<'_> {
// The number of clusters from progress_chars to write (rounding down).
let width = width / self.char_width;
// The number of full clusters (including a fractional component for a partially-full one).
let fill = fract * width as f32;
// The number of entirely full clusters (by truncating `fill`).
let entirely_filled = fill as usize;
// 1 if the bar is not entirely empty or full (meaning we need to draw the "current"
// character between the filled and "to do" segment), 0 otherwise.
let head = if fill > 0.0 && entirely_filled < width {
1
} else {
0
};
let cur = if head == 1 {
// Number of fine-grained progress entries in progress_chars.
let n = self.progress_chars.len().saturating_sub(2);
let cur_char = if n <= 1 {
// No fine-grained entries. 1 is the single "current" entry if we have one, the "to
// do" entry if not.
1
} else {
// Pick a fine-grained entry, ranging from the last one (n) if the fractional part
// of fill is 0 to the first one (1) if the fractional part of fill is almost 1.
n.saturating_sub((fill.fract() * n as f32) as usize)
};
Some(cur_char)
} else {
None
};
// Number of entirely empty clusters needed to fill the bar up to `width`.
let bg = width.saturating_sub(entirely_filled).saturating_sub(head);
let rest = RepeatedStringDisplay {
str: &self.progress_chars[self.progress_chars.len() - 1],
num: bg,
};
BarDisplay {
chars: &self.progress_chars,
filled: entirely_filled,
cur,
rest: alt_style.unwrap_or(&Style::new()).apply_to(rest),
}
}
pub(crate) fn format_state(
&self,
state: &ProgressState,
lines: &mut Vec<String>,
target_width: u16,
) {
let mut cur = String::new();
let mut buf = String::new();
let mut wide = None;
let pos = state.pos();
let len = state.len().unwrap_or(pos);
for part in &self.template.parts {
match part {
TemplatePart::Placeholder {
key,
align,
width,
truncate,
style,
alt_style,
} => {
buf.clear();
if let Some(tracker) = self.format_map.get(key.as_str()) {
tracker.write(state, &mut TabRewriter(&mut buf, self.tab_width));
} else {
match key.as_str() {
"wide_bar" => {
wide = Some(WideElement::Bar { alt_style });
buf.push('\x00');
}
"bar" => buf
.write_fmt(format_args!(
"{}",
self.format_bar(
state.fraction(),
width.unwrap_or(20) as usize,
alt_style.as_ref(),
)
))
.unwrap(),
"spinner" => buf.push_str(self.current_tick_str(state)),
"wide_msg" => {
wide = Some(WideElement::Message { align });
buf.push('\x00');
}
"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()
}
"len" => buf.write_fmt(format_args!("{}", len)).unwrap(),
"human_len" => {
buf.write_fmt(format_args!("{}", HumanCount(len))).unwrap()
}
"percent" => buf
.write_fmt(format_args!("{:.*}", 0, state.fraction() * 100f32))
.unwrap(),
"bytes" => buf.write_fmt(format_args!("{}", HumanBytes(pos))).unwrap(),
"total_bytes" => {
buf.write_fmt(format_args!("{}", HumanBytes(len))).unwrap()
}
"decimal_bytes" => buf
.write_fmt(format_args!("{}", DecimalBytes(pos)))
.unwrap(),
"decimal_total_bytes" => buf
.write_fmt(format_args!("{}", DecimalBytes(len)))
.unwrap(),
"binary_bytes" => {
buf.write_fmt(format_args!("{}", BinaryBytes(pos))).unwrap()
}
"binary_total_bytes" => {
buf.write_fmt(format_args!("{}", BinaryBytes(len))).unwrap()
}
"elapsed_precise" => buf
.write_fmt(format_args!("{}", FormattedDuration(state.elapsed())))
.unwrap(),
"elapsed" => buf
.write_fmt(format_args!("{:#}", HumanDuration(state.elapsed())))
.unwrap(),
"per_sec" => buf
.write_fmt(format_args!("{:.4}/s", state.per_sec()))
.unwrap(),
"bytes_per_sec" => buf
.write_fmt(format_args!("{}/s", HumanBytes(state.per_sec() as u64)))
.unwrap(),
"binary_bytes_per_sec" => buf
.write_fmt(format_args!(
"{}/s",
BinaryBytes(state.per_sec() as u64)
))
.unwrap(),
"eta_precise" => buf
.write_fmt(format_args!("{}", FormattedDuration(state.eta())))
.unwrap(),
"eta" => buf
.write_fmt(format_args!("{:#}", HumanDuration(state.eta())))
.unwrap(),
"duration_precise" => buf
.write_fmt(format_args!("{}", FormattedDuration(state.duration())))
.unwrap(),
"duration" => buf
.write_fmt(format_args!("{:#}", HumanDuration(state.duration())))
.unwrap(),
_ => (),
}
};
match width {
Some(width) => {
let padded = PaddedStringDisplay {
str: &buf,
width: *width as usize,
align: *align,
truncate: *truncate,
};
match style {
Some(s) => cur
.write_fmt(format_args!("{}", s.apply_to(padded)))
.unwrap(),
None => cur.write_fmt(format_args!("{}", padded)).unwrap(),
}
}
None => match style {
Some(s) => cur.write_fmt(format_args!("{}", s.apply_to(&buf))).unwrap(),
None => cur.push_str(&buf),
},
}
}
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)
}
None => mem::take(&mut cur),
}),
}
}
if !cur.is_empty() {
lines.push(match wide {
Some(inner) => {
inner.expand(mem::take(&mut cur), self, state, &mut buf, target_width)
}
None => mem::take(&mut cur),
})
}
}
}
struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize);
impl Write for TabRewriter<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0
.write_str(s.replace('\t', &" ".repeat(self.1)).as_str())
}
}
#[derive(Clone, Copy)]
enum WideElement<'a> {
Bar { alt_style: &'a Option<Style> },
Message { align: &'a Alignment },
}
impl<'a> WideElement<'a> {
fn expand(
self,
cur: String,
style: &ProgressStyle,
state: &ProgressState,
buf: &mut String,
width: u16,
) -> String {
let left = (width as usize).saturating_sub(measure_text_width(&*cur.replace('\x00', "")));
match self {
Self::Bar { alt_style } => cur.replace(
'\x00',
&format!(
"{}",
style.format_bar(state.fraction(), left, alt_style.as_ref())
),
),
WideElement::Message { align } => {
buf.clear();
buf.write_fmt(format_args!(
"{}",
PaddedStringDisplay {
str: state.message.expanded(),
width: left,
align: *align,
truncate: true,
}
))
.unwrap();
let trimmed = match cur.as_bytes().last() == Some(&b'\x00') {
true => buf.trim_end(),
false => buf,
};
cur.replace('\x00', trimmed)
}
}
}
}
#[derive(Clone, Debug)]
struct Template {
parts: Vec<TemplatePart>,
}
impl Template {
fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> {
use State::*;
let (mut state, mut parts, mut buf) = (Literal, vec![], String::new());
for c in s.chars() {
let new = match (state, c) {
(Literal, '{') => (MaybeOpen, None),
(Literal, '\n') => {
if !buf.is_empty() {
parts.push(TemplatePart::Literal(TabExpandedString::new(
mem::take(&mut buf).into(),
tab_width,
)));
}
parts.push(TemplatePart::NewLine);
(Literal, None)
}
(Literal, '}') => (DoubleClose, Some('}')),
(Literal, c) => (Literal, Some(c)),
(DoubleClose, '}') => (Literal, None),
(MaybeOpen, '{') => (Literal, Some('{')),
(MaybeOpen, c) | (Key, c) if c.is_ascii_whitespace() => {
// If we find whitespace where the variable key is supposed to go,
// backtrack and act as if this was a literal.
buf.push(c);
let mut new = String::from("{");
new.push_str(&buf);
buf.clear();
parts.push(TemplatePart::Literal(TabExpandedString::new(
new.into(),
tab_width,
)));
(Literal, None)
}
(MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)),
(Key, c) if c != '}' && c != ':' => (Key, Some(c)),
(Key, ':') => (Align, None),
(Key, '}') => (Literal, None),
(Key, '!') if !buf.is_empty() => {
parts.push(TemplatePart::Placeholder {
key: mem::take(&mut buf),
align: Alignment::Left,
width: None,
truncate: true,
style: None,
alt_style: None,
});
(Width, None)
}
(Align, c) if c == '<' || c == '^' || c == '>' => {
if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() {
match c {
'<' => *align = Alignment::Left,
'^' => *align = Alignment::Center,
'>' => *align = Alignment::Right,
_ => (),
}
}
(Width, None)
}
(Align, c @ '0'..='9') => (Width, Some(c)),
(Align, '!') | (Width, '!') => {
if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() {
*truncate = true;
}
(Width, None)
}
(Align, '.') => (FirstStyle, None),
(Align, '}') => (Literal, None),
(Width, c @ '0'..='9') => (Width, Some(c)),
(Width, '.') => (FirstStyle, None),
(Width, '}') => (Literal, None),
(FirstStyle, '/') => (AltStyle, None),
(FirstStyle, '}') => (Literal, None),
(FirstStyle, c) => (FirstStyle, Some(c)),
(AltStyle, '}') => (Literal, None),
(AltStyle, c) => (AltStyle, Some(c)),
(st, c) => return Err(TemplateError { next: c, state: st }),
};
match (state, new.0) {
(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),
align: Alignment::Left,
width: None,
truncate: false,
style: None,
alt_style: None,
})
}
(Width, FirstStyle) | (Width, Literal) if !buf.is_empty() => {
if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() {
*width = Some(buf.parse().unwrap());
buf.clear();
}
}
(FirstStyle, AltStyle) | (FirstStyle, Literal) if !buf.is_empty() => {
if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() {
*style = Some(Style::from_dotted_str(&buf));
buf.clear();
}
}
(AltStyle, Literal) if !buf.is_empty() => {
if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() {
*alt_style = Some(Style::from_dotted_str(&buf));
buf.clear();
}
}
(_, _) => (),
}
state = new.0;
if let Some(c) = new.1 {
buf.push(c);
}
}
if matches!(state, Literal | DoubleClose) && !buf.is_empty() {
parts.push(TemplatePart::Literal(TabExpandedString::new(
buf.into(),
tab_width,
)));
}
Ok(Self { parts })
}
fn from_str(s: &str) -> Result<Self, TemplateError> {
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)]
pub struct TemplateError {
state: State,
next: char,
}
impl fmt::Display for TemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TemplateError: unexpected character {:?} in state {:?}",
self.next, self.state
)
}
}
impl std::error::Error for TemplateError {}
#[derive(Clone, Debug, PartialEq, Eq)]
enum TemplatePart {
Literal(TabExpandedString),
Placeholder {
key: String,
align: Alignment,
width: Option<u16>,
truncate: bool,
style: Option<Style>,
alt_style: Option<Style>,
},
NewLine,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum State {
Literal,
MaybeOpen,
DoubleClose,
Key,
Align,
Width,
FirstStyle,
AltStyle,
}
struct BarDisplay<'a> {
chars: &'a [Box<str>],
filled: usize,
cur: Option<usize>,
rest: console::StyledObject<RepeatedStringDisplay<'a>>,
}
impl<'a> fmt::Display for BarDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for _ in 0..self.filled {
f.write_str(&self.chars[0])?;
}
if let Some(cur) = self.cur {
f.write_str(&self.chars[cur])?;
}
self.rest.fmt(f)
}
}
struct RepeatedStringDisplay<'a> {
str: &'a str,
num: usize,
}
impl<'a> fmt::Display for RepeatedStringDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for _ in 0..self.num {
f.write_str(self.str)?;
}
Ok(())
}
}
struct PaddedStringDisplay<'a> {
str: &'a str,
width: usize,
align: Alignment,
truncate: bool,
}
impl<'a> fmt::Display for PaddedStringDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let cols = measure_text_width(self.str);
let excess = cols.saturating_sub(self.width);
if excess > 0 && !self.truncate {
return f.write_str(self.str);
} else if excess > 0 {
let (start, end) = match self.align {
Alignment::Left => (0, self.str.len() - excess),
Alignment::Right => (excess, self.str.len()),
Alignment::Center => (
excess / 2,
self.str.len() - excess.saturating_sub(excess / 2),
),
};
return f.write_str(self.str.get(start..end).unwrap_or(self.str));
}
let diff = self.width.saturating_sub(cols);
let (left_pad, right_pad) = match self.align {
Alignment::Left => (0, diff),
Alignment::Right => (diff, 0),
Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)),
};
for _ in 0..left_pad {
f.write_char(' ')?;
}
f.write_str(self.str)?;
for _ in 0..right_pad {
f.write_char(' ')?;
}
Ok(())
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
enum Alignment {
Left,
Center,
Right,
}
/// Trait for defining stateful or stateless formatters
pub trait ProgressTracker: Send {
/// Creates a new instance of the progress tracker
fn clone_box(&self) -> Box<dyn ProgressTracker>;
/// Notifies the progress tracker of a tick event
fn tick(&mut self, state: &ProgressState, now: Instant);
/// Notifies the progress tracker of a reset event
fn reset(&mut self, state: &ProgressState, now: Instant);
/// Provides access to the progress bar display buffer for custom messages
fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write);
}
impl Clone for Box<dyn ProgressTracker> {
fn clone(&self) -> Self {
self.clone_box()
}
}
impl<F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Clone + 'static> ProgressTracker for F {
fn clone_box(&self) -> Box<dyn ProgressTracker> {
Box::new(self.clone())
}
fn tick(&mut self, _: &ProgressState, _: Instant) {}
fn reset(&mut self, _: &ProgressState, _: Instant) {}
fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) {
(self)(state, w)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::state::{AtomicPosition, ProgressState};
use std::sync::Mutex;
#[test]
fn test_stateful_tracker() {
#[derive(Debug, Clone)]
struct TestTracker(Arc<Mutex<String>>);
impl ProgressTracker for TestTracker {
fn clone_box(&self) -> Box<dyn ProgressTracker> {
Box::new(self.clone())
}
fn tick(&mut self, state: &ProgressState, _: Instant) {
let mut m = self.0.lock().unwrap();
m.clear();
m.push_str(format!("{} {}", state.len().unwrap(), state.pos()).as_str());
}
fn reset(&mut self, _state: &ProgressState, _: Instant) {
let mut m = self.0.lock().unwrap();
m.clear();
}
fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) {
w.write_str(self.0.lock().unwrap().as_str()).unwrap()
}
}
use crate::ProgressBar;
let pb = ProgressBar::new(1);
pb.set_style(
ProgressStyle::with_template("{{ {foo} }}")
.unwrap()
.with_key("foo", TestTracker(Arc::new(Mutex::new(String::default()))))
.progress_chars("#>-"),
);
let mut buf = Vec::new();
let style = pb.clone().style();
style.format_state(&pb.state().state, &mut buf, 16);
assert_eq!(&buf[0], "{ }");
buf.clear();
pb.inc(1);
style.format_state(&pb.state().state, &mut buf, 16);
assert_eq!(&buf[0], "{ 1 1 }");
pb.reset();
buf.clear();
style.format_state(&pb.state().state, &mut buf, 16);
assert_eq!(&buf[0], "{ }");
pb.finish_and_clear();
}
use crate::state::TabExpandedString;
#[test]
fn test_expand_template() {
const WIDTH: u16 = 80;
let pos = Arc::new(AtomicPosition::new());
let state = ProgressState::new(Some(10), pos);
let mut buf = Vec::new();
let mut style = ProgressStyle::default_bar();
style.format_map.insert(
"foo",
Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO").unwrap()),
);
style.format_map.insert(
"bar",
Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR").unwrap()),
);
style.template = Template::from_str("{{ {foo} {bar} }}").unwrap();
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "{ FOO BAR }");
buf.clear();
style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"#).unwrap();
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"#);
}
#[test]
fn test_expand_template_flags() {
use console::set_colors_enabled;
set_colors_enabled(true);
const WIDTH: u16 = 80;
let pos = Arc::new(AtomicPosition::new());
let state = ProgressState::new(Some(10), pos);
let mut buf = Vec::new();
let mut style = ProgressStyle::default_bar();
style.format_map.insert(
"foo",
Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX").unwrap()),
);
style.template = Template::from_str("{foo:5}").unwrap();
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "XXX ");
buf.clear();
style.template = Template::from_str("{foo:.red.on_blue}").unwrap();
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44mXXX\u{1b}[0m");
buf.clear();
style.template = Template::from_str("{foo:^5.red.on_blue}").unwrap();
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
buf.clear();
style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}").unwrap();
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "\u{1b}[31m\u{1b}[44m XXX \u{1b}[0m");
}
#[test]
fn align_truncation() {
const WIDTH: u16 = 10;
let pos = Arc::new(AtomicPosition::new());
let mut state = ProgressState::new(Some(10), pos);
let mut buf = Vec::new();
let style = ProgressStyle::with_template("{wide_msg}").unwrap();
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 = 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 = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into());
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "fghijklmno");
}
#[test]
fn wide_element_style() {
const CHARS: &str = "=>-";
const WIDTH: u16 = 8;
let pos = Arc::new(AtomicPosition::new());
// half finished
pos.set(2);
let mut state = ProgressState::new(Some(4), pos);
let mut buf = Vec::new();
let style = ProgressStyle::with_template("{wide_bar}")
.unwrap()
.progress_chars(CHARS);
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(&buf[0], "====>---");
buf.clear();
let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}")
.unwrap()
.progress_chars(CHARS);
style.format_state(&state, &mut buf, WIDTH);
assert_eq!(
&buf[0],
"\u{1b}[31m\u{1b}[44m====>\u{1b}[32m\u{1b}[46m---\u{1b}[0m\u{1b}[0m"
);
buf.clear();
let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap();
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");
}
}