use std::borrow::Cow;
use std::fmt::Write as _;
use std::time::Duration;
use owo_colors::{OwoColorize as _, Style};
use crate::bar::Bar;
pub struct RenderContext<'a> {
pub spinner: Option<char>,
pub elapsed: Duration,
pub show_elapsed: bool,
pub bar: &'a Bar<'a>,
pub bar_width: usize,
pub progress: Option<f64>,
pub label: Option<&'a str>,
pub message: Option<&'a str>,
pub spinner_style: Style,
pub annotation_style: Style,
}
#[derive(Clone)]
pub enum Segment {
Spinner {
style: Option<Style>,
},
Elapsed {
style: Option<Style>,
border: Option<(Cow<'static, str>, Cow<'static, str>)>,
precision: u8,
},
Bar,
Label {
style: Option<Style>,
width: Option<usize>,
},
Message,
Literal(Cow<'static, str>),
Custom(fn(&RenderContext, &mut String)),
}
impl Segment {
pub const fn spinner() -> Self {
Segment::Spinner { style: None }
}
pub const fn elapsed() -> Self {
Segment::Elapsed {
style: None,
border: None,
precision: 2,
}
}
pub const fn bar() -> Self {
Segment::Bar
}
pub const fn label() -> Self {
Segment::Label {
style: None,
width: None,
}
}
pub const fn message() -> Self {
Segment::Message
}
pub fn literal(text: impl Into<Cow<'static, str>>) -> Self {
Segment::Literal(text.into())
}
pub fn custom(f: fn(&RenderContext, &mut String)) -> Self {
Segment::Custom(f)
}
pub fn with_style(self, style: Style) -> Self {
match self {
Segment::Spinner { .. } => Segment::Spinner { style: Some(style) },
Segment::Elapsed {
border, precision, ..
} => Segment::Elapsed {
style: Some(style),
border,
precision,
},
Segment::Label { width, .. } => Segment::Label {
style: Some(style),
width,
},
other => other,
}
}
pub fn with_border(
self,
left: impl Into<Cow<'static, str>>,
right: impl Into<Cow<'static, str>>,
) -> Self {
match self {
Segment::Elapsed {
style, precision, ..
} => Segment::Elapsed {
style,
border: Some((left.into(), right.into())),
precision,
},
other => other,
}
}
pub fn with_precision(self, precision: u8) -> Self {
match self {
Segment::Elapsed { style, border, .. } => Segment::Elapsed {
style,
border,
precision,
},
other => other,
}
}
pub fn with_width(self, width: usize) -> Self {
match self {
Segment::Label { style, .. } => Segment::Label {
style,
width: Some(width),
},
other => other,
}
}
fn render(&self, ctx: &RenderContext, buf: &mut String) {
match self {
Segment::Spinner { style } => {
if let Some(ch) = ctx.spinner {
let style = style.unwrap_or(ctx.spinner_style);
let _ = write!(buf, "{}", ch.style(style));
}
}
Segment::Elapsed {
style,
border,
precision,
} => {
if !ctx.show_elapsed {
return;
}
match style {
Some(style) => {
let mut token = String::new();
if let Some((left, _)) = border {
token.push_str(left);
}
let _ = write!(
token,
"{:.*}s",
*precision as usize,
ctx.elapsed.as_secs_f64()
);
if let Some((_, right)) = border {
token.push_str(right);
}
let _ = write!(buf, "{}", token.style(*style));
}
None => {
if let Some((left, _)) = border {
buf.push_str(left);
}
let _ = write!(
buf,
"{:.*}s",
*precision as usize,
ctx.elapsed.as_secs_f64()
);
if let Some((_, right)) = border {
buf.push_str(right);
}
}
}
}
Segment::Bar => {
if let Some(progress) = ctx.progress {
ctx.bar.render_into(buf, ctx.bar_width, progress);
}
}
Segment::Label { style, width } => {
if let Some(label) = ctx.label {
let style = style.unwrap_or(ctx.annotation_style);
match width {
Some(width) => {
let width = *width;
let _ = write!(
buf,
"{}",
format_args!("{label:<width$.width$}").style(style)
);
}
None => {
let _ = write!(buf, "{}", label.style(style));
}
}
}
}
Segment::Message => {
if let Some(message) = ctx.message {
buf.push_str(message);
}
}
Segment::Literal(text) => buf.push_str(text),
Segment::Custom(f) => f(ctx, buf),
}
}
}
#[derive(Clone)]
pub struct Layout {
segments: Cow<'static, [Segment]>,
separator: Cow<'static, str>,
}
static DEFAULT_SEGMENTS: [Segment; 5] = [
Segment::Spinner { style: None },
Segment::Elapsed {
style: None,
border: None,
precision: 2,
},
Segment::Label {
style: None,
width: None,
},
Segment::Bar,
Segment::Message,
];
impl Layout {
pub const DEFAULT: Layout = Layout::new(&DEFAULT_SEGMENTS);
pub const fn new(segments: &'static [Segment]) -> Self {
Self {
segments: Cow::Borrowed(segments),
separator: Cow::Borrowed(" "),
}
}
pub fn with_segment(mut self, segment: Segment) -> Self {
self.segments.to_mut().push(segment);
self
}
pub fn with_separator(mut self, separator: impl Into<Cow<'static, str>>) -> Self {
self.separator = separator.into();
self
}
pub fn render(&self, ctx: &RenderContext, buf: &mut String) {
let mut first = true;
for segment in self.segments.iter() {
let rollback = buf.len();
if !first {
buf.push_str(&self.separator);
}
let after_separator = buf.len();
segment.render(ctx, buf);
if buf.len() == after_separator {
buf.truncate(rollback);
} else {
first = false;
}
}
}
}
impl Default for Layout {
fn default() -> Self {
Layout::DEFAULT
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bar::Bar;
fn context() -> RenderContext<'static> {
RenderContext {
spinner: None,
elapsed: Duration::from_millis(1500),
show_elapsed: false,
bar: EMPTY_BAR,
bar_width: 10,
progress: None,
label: None,
message: None,
spinner_style: Style::new(),
annotation_style: Style::new(),
}
}
static EMPTY_BAR: &Bar<'static> = &Bar::empty();
#[test]
fn skips_empty_segments_and_their_separators() {
let layout = Layout::new(&[])
.with_segment(Segment::elapsed())
.with_segment(Segment::message())
.with_segment(Segment::bar())
.with_segment(Segment::literal("done"));
let mut ctx = context();
ctx.message = Some("hello");
let mut buf = String::new();
layout.render(&ctx, &mut buf);
assert_eq!(buf, "hello done");
}
#[test]
fn elapsed_border_and_precision() {
let layout = Layout::new(&[])
.with_segment(Segment::elapsed().with_border("[", "]").with_precision(1));
let mut ctx = context();
ctx.show_elapsed = true;
let mut buf = String::new();
layout.render(&ctx, &mut buf);
assert_eq!(buf, "[1.5s]");
}
#[test]
fn custom_separator() {
let layout = Layout::new(&[])
.with_segment(Segment::literal("a"))
.with_segment(Segment::literal("b"))
.with_separator(" | ");
let mut buf = String::new();
layout.render(&context(), &mut buf);
assert_eq!(buf, "a | b");
}
}