use crate::bar::state::BarState;
use crate::bar::template::{ParsedTemplate, SharedTemplateKey, TemplateError, parse_template};
use crate::style::color::ColorSpec;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProgressChars {
pub fill: char,
pub head: char,
pub empty: char,
}
impl ProgressChars {
pub fn new(fill: char, head: char, empty: char) -> Self {
Self { fill, head, empty }
}
pub fn parse(s: &str) -> Option<Self> {
let mut chars = s.chars();
let fill = chars.next()?;
let head = chars.next()?;
let empty = chars.next()?;
Some(Self { fill, head, empty })
}
pub fn ascii() -> Self {
Self::new('#', '>', '-')
}
pub fn equals() -> Self {
Self::new('=', '>', '-')
}
pub fn blocks() -> Self {
Self::new('█', '▉', '░')
}
pub fn dots() -> Self {
Self::new('•', '∘', ' ')
}
pub fn as_string(&self) -> String {
let mut out = String::new();
out.push(self.fill);
out.push(self.head);
out.push(self.empty);
out
}
}
pub struct ProgressStyle {
pub(crate) template: String,
pub(crate) parsed_template: ParsedTemplate,
pub(crate) progress_chars: String,
pub(crate) tick_strings: Vec<String>,
pub(crate) color_spec: Option<ColorSpec>,
pub(crate) custom_keys: HashMap<String, SharedTemplateKey>,
}
impl ProgressStyle {
pub fn default_bar() -> Self {
Self::with_template("{prefix:.bold} [{bar:40.green/red}] {pos}/{len} ({percent}%) {msg}")
.unwrap_or_else_fallback()
.progress_chars("#>-")
}
pub fn default_spinner() -> Self {
Self::with_template("{prefix:.bold} {spinner} {msg}")
.unwrap_or_else_fallback()
.tick_strings(&["-", "\\", "|", "/"])
}
pub fn with_template(template: &str) -> Result<Self, TemplateError> {
let parsed_template = parse_template(template)?;
Ok(Self {
template: template.to_string(),
parsed_template,
progress_chars: "#>-".to_string(),
tick_strings: vec!["-".into(), "\\".into(), "|".into(), "/".into()],
color_spec: None,
custom_keys: HashMap::new(),
})
}
pub fn progress_chars(mut self, s: &str) -> Self {
if s.chars().count() >= 3 {
self.progress_chars = s.to_string();
}
self
}
pub fn progress_chars_set(mut self, chars: ProgressChars) -> Self {
self.progress_chars = chars.as_string();
self
}
pub fn try_progress_chars(mut self, s: &str) -> Result<Self, TemplateError> {
if let Some(chars) = ProgressChars::parse(s) {
self.progress_chars = chars.as_string();
Ok(self)
} else {
Err(TemplateError(
"progress character set must contain at least three characters".into(),
))
}
}
pub fn tick_chars(mut self, s: &str) -> Self {
let frames: Vec<String> = s.chars().map(|ch| ch.to_string()).collect();
if !frames.is_empty() {
self.tick_strings = frames;
}
self
}
pub fn tick_strings(mut self, frames: &[&str]) -> Self {
if !frames.is_empty() {
self.tick_strings = frames.iter().map(|frame| (*frame).to_string()).collect();
}
self
}
pub fn template(mut self, template: &str) -> Result<Self, TemplateError> {
self.parsed_template = parse_template(template)?;
self.template = template.to_string();
Ok(self)
}
pub fn with_key(
mut self,
key: impl Into<String>,
f: impl Fn(&BarState) -> String + Send + Sync + 'static,
) -> Self {
self.custom_keys.insert(key.into(), Arc::new(f));
self
}
pub fn color(mut self, spec: ColorSpec) -> Self {
self.color_spec = Some(spec);
self
}
pub fn template_string(&self) -> &str {
&self.template
}
pub fn progress_chars_string(&self) -> &str {
&self.progress_chars
}
pub fn tick_frame_count(&self) -> usize {
self.tick_strings.len()
}
pub(crate) fn get_tick_frame(&self, idx: usize) -> &str {
if self.tick_strings.is_empty() {
" "
} else {
self.tick_strings[idx % self.tick_strings.len()].as_str()
}
}
pub(crate) fn fill_char(&self) -> char {
self.progress_chars.chars().next().unwrap_or('#')
}
pub(crate) fn head_char(&self) -> char {
self.progress_chars.chars().nth(1).unwrap_or('>')
}
pub(crate) fn empty_char(&self) -> char {
self.progress_chars.chars().last().unwrap_or('-')
}
pub(crate) fn render_bar_segment(&self, width: usize, fraction: f64) -> String {
if width == 0 {
return String::new();
}
let fraction = if fraction.is_finite() {
fraction.clamp(0.0, 1.0)
} else {
0.0
};
let fill = self.fill_char();
let head = self.head_char();
let empty = self.empty_char();
if fraction <= 0.0 {
return empty.to_string().repeat(width);
}
if fraction >= 1.0 {
return fill.to_string().repeat(width);
}
let filled = ((width as f64) * fraction).floor() as usize;
let filled = filled.min(width.saturating_sub(1));
let mut out = fill.to_string().repeat(filled);
out.push(head);
out.push_str(&empty.to_string().repeat(width.saturating_sub(filled + 1)));
out
}
}
trait FallbackStyle {
fn unwrap_or_else_fallback(self) -> ProgressStyle;
}
impl FallbackStyle for Result<ProgressStyle, TemplateError> {
fn unwrap_or_else_fallback(self) -> ProgressStyle {
match self {
Ok(style) => style,
Err(_) => ProgressStyle {
template: "{pos}/{len}".to_string(),
parsed_template: ParsedTemplate {
parts: vec![
crate::bar::template::TemplatePart::Variable(
crate::bar::template::TemplateVar {
name: "pos".to_string(),
format_spec: None,
},
),
crate::bar::template::TemplatePart::Literal("/".to_string()),
crate::bar::template::TemplatePart::Variable(
crate::bar::template::TemplateVar {
name: "len".to_string(),
format_spec: None,
},
),
],
},
progress_chars: "#>-".to_string(),
tick_strings: vec!["-".into(), "\\".into(), "|".into(), "/".into()],
color_spec: None,
custom_keys: HashMap::new(),
},
}
}
}
impl Clone for ProgressStyle {
fn clone(&self) -> Self {
Self {
template: self.template.clone(),
parsed_template: self.parsed_template.clone(),
progress_chars: self.progress_chars.clone(),
tick_strings: self.tick_strings.clone(),
color_spec: self.color_spec.clone(),
custom_keys: self
.custom_keys
.iter()
.map(|(key, value)| (key.clone(), Arc::clone(value)))
.collect(),
}
}
}
impl fmt::Debug for ProgressStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProgressStyle")
.field("template", &self.template)
.field("progress_chars", &self.progress_chars)
.field("tick_strings", &self.tick_strings)
.field("color_spec", &self.color_spec)
.field("custom_keys", &self.custom_keys.keys().collect::<Vec<_>>())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_bar_parses() {
assert!(
!ProgressStyle::default_bar()
.parsed_template
.parts
.is_empty()
);
}
#[test]
fn test_default_spinner_parses() {
assert!(
!ProgressStyle::default_spinner()
.parsed_template
.parts
.is_empty()
);
}
#[test]
fn test_custom_template() -> Result<(), TemplateError> {
let style = ProgressStyle::with_template("{pos}")?;
assert_eq!(style.template, "{pos}");
Ok(())
}
#[test]
fn test_invalid_template_error() {
assert!(ProgressStyle::with_template("{pos").is_err());
}
#[test]
fn test_progress_chars_3_chars() {
let style = ProgressStyle::default_bar().progress_chars("=>-");
assert_eq!(style.progress_chars, "=>-");
}
#[test]
fn test_progress_chars_set() {
let style = ProgressStyle::default_bar().progress_chars_set(ProgressChars::blocks());
assert_eq!(style.progress_chars, "█▉░");
}
#[test]
fn test_try_progress_chars_errors_for_short_input() {
assert!(
ProgressStyle::default_bar()
.try_progress_chars("=")
.is_err()
);
}
#[test]
fn test_progress_chars_parse() {
assert_eq!(
ProgressChars::parse("=>-"),
Some(ProgressChars::new('=', '>', '-'))
);
}
#[test]
fn test_style_introspection() -> Result<(), TemplateError> {
let style = ProgressStyle::with_template("{bar}")?.tick_strings(&["a", "b"]);
assert_eq!(style.template_string(), "{bar}");
assert_eq!(style.progress_chars_string(), "#>-");
assert_eq!(style.tick_frame_count(), 2);
Ok(())
}
#[test]
fn test_progress_chars_fewer_than_3_errors() {
let style = ProgressStyle::default_bar().progress_chars("=");
assert_eq!(style.progress_chars, "#>-");
}
#[test]
fn test_tick_strings() {
let style = ProgressStyle::default_spinner().tick_strings(&["a", "b"]);
assert_eq!(style.get_tick_frame(1), "b");
}
#[test]
fn test_render_bar_empty() {
assert_eq!(
ProgressStyle::default_bar().render_bar_segment(5, 0.0),
"-----"
);
}
#[test]
fn test_render_bar_half() {
assert_eq!(
ProgressStyle::default_bar().render_bar_segment(5, 0.5),
"##>--"
);
}
#[test]
fn test_render_bar_full() {
assert_eq!(
ProgressStyle::default_bar().render_bar_segment(5, 1.0),
"#####"
);
}
#[test]
fn test_custom_key_callable() {
let style = ProgressStyle::default_bar().with_key("x", |state| state.pos.to_string());
let state = BarState::new(Some(1), style.clone());
let value = style.custom_keys.get("x").map(|f| f(&state));
assert_eq!(value.as_deref(), Some("0"));
}
}