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 bytes_done: u64,
pub bytes_total: Option<u64>,
pub rate: 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,
Bytes,
Rate,
Eta,
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 const fn bytes() -> Self {
Segment::Bytes
}
pub const fn rate() -> Self {
Segment::Rate
}
pub const fn eta() -> Self {
Segment::Eta
}
pub const fn literal(text: &'static str) -> Self {
Segment::Literal(Cow::Borrowed(text))
}
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::Bytes => {
if ctx.bytes_done == 0 && ctx.bytes_total.is_none() {
return;
}
format_bytes_iec(ctx.bytes_done, buf);
if let Some(total) = ctx.bytes_total {
buf.push_str(" / ");
format_bytes_iec(total, buf);
}
}
Segment::Rate => {
if let Some(rate) = ctx.rate {
format_bytes_iec(rate.max(0.0) as u64, buf);
buf.push_str("/s");
}
}
Segment::Eta => {
if let (Some(total), Some(rate)) = (ctx.bytes_total, ctx.rate) {
if rate > 0.0 && total > ctx.bytes_done {
let remaining = (total - ctx.bytes_done) as f64 / rate;
format_eta_secs(remaining, buf);
}
}
}
Segment::Literal(text) => buf.push_str(text),
Segment::Custom(f) => f(ctx, buf),
}
}
}
const BYTE_UNITS: [&str; 6] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
fn format_bytes_iec(n: u64, buf: &mut String) {
if n < 1024 {
let _ = write!(buf, "{n} B");
return;
}
let mut value = n as f64;
let mut unit = 0;
while value >= 1024.0 && unit < BYTE_UNITS.len() - 1 {
value /= 1024.0;
unit += 1;
}
let _ = write!(buf, "{:.2} {}", value, BYTE_UNITS[unit]);
}
fn format_eta_secs(secs: f64, buf: &mut String) {
if !secs.is_finite() || secs < 0.0 {
return;
}
let total = secs as u64;
let hours = total / 3600;
let minutes = (total % 3600) / 60;
let seconds = total % 60;
buf.push_str("eta ");
if hours > 0 {
let _ = write!(buf, "{hours}h{minutes:02}m{seconds:02}s");
} else if minutes > 0 {
let _ = write!(buf, "{minutes}m{seconds:02}s");
} else {
let _ = write!(buf, "{seconds}s");
}
}
#[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,
bytes_done: 0,
bytes_total: None,
rate: 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");
}
fn render(segment: Segment, ctx: &RenderContext) -> String {
let mut buf = String::new();
Layout::new(&[]).with_segment(segment).render(ctx, &mut buf);
buf
}
#[test]
fn bytes_segment_skips_when_zero_and_no_total() {
assert_eq!(render(Segment::bytes(), &context()), "");
}
#[test]
fn bytes_segment_renders_done_only() {
let mut ctx = context();
ctx.bytes_done = 1500;
assert_eq!(render(Segment::bytes(), &ctx), "1.46 KiB");
}
#[test]
fn bytes_segment_renders_done_and_total() {
let mut ctx = context();
ctx.bytes_done = 1024 * 1024;
ctx.bytes_total = Some(5 * 1024 * 1024);
assert_eq!(render(Segment::bytes(), &ctx), "1.00 MiB / 5.00 MiB");
}
#[test]
fn bytes_segment_renders_zero_when_total_known() {
let mut ctx = context();
ctx.bytes_total = Some(2048);
assert_eq!(render(Segment::bytes(), &ctx), "0 B / 2.00 KiB");
}
#[test]
fn rate_segment_skips_without_sample() {
assert_eq!(render(Segment::rate(), &context()), "");
}
#[test]
fn rate_segment_renders_with_unit_suffix() {
let mut ctx = context();
ctx.rate = Some(800.0 * 1024.0);
assert_eq!(render(Segment::rate(), &ctx), "800.00 KiB/s");
}
#[test]
fn eta_segment_skips_without_total_or_rate() {
let mut ctx = context();
ctx.rate = Some(1024.0);
assert_eq!(render(Segment::eta(), &ctx), "");
ctx.rate = None;
ctx.bytes_total = Some(2048);
assert_eq!(render(Segment::eta(), &ctx), "");
}
#[test]
fn eta_segment_seconds() {
let mut ctx = context();
ctx.bytes_done = 0;
ctx.bytes_total = Some(1024 * 50);
ctx.rate = Some(1024.0 * 10.0);
assert_eq!(render(Segment::eta(), &ctx), "eta 5s");
}
#[test]
fn eta_segment_minutes_and_hours() {
let mut ctx = context();
ctx.bytes_done = 0;
ctx.bytes_total = Some(125);
ctx.rate = Some(1.0);
assert_eq!(render(Segment::eta(), &ctx), "eta 2m05s");
ctx.bytes_total = Some(3725);
assert_eq!(render(Segment::eta(), &ctx), "eta 1h02m05s");
}
#[test]
fn eta_segment_skips_when_complete() {
let mut ctx = context();
ctx.bytes_done = 4096;
ctx.bytes_total = Some(4096);
ctx.rate = Some(1024.0);
assert_eq!(render(Segment::eta(), &ctx), "");
}
}