use crate::progress::Task;
use crate::progress::spinner::{Spinner, SpinnerStyle};
use crate::style::{Color, Style};
use crate::text::Span;
use std::fmt::Debug;
use std::time::Duration;
pub trait ProgressColumn: Send + Sync + Debug {
fn render(&self, task: &Task) -> Vec<Span>;
fn render_with_width(&self, task: &Task, _available_width: Option<usize>) -> Vec<Span> {
self.render(task)
}
fn is_expandable(&self) -> bool {
false
}
fn min_width(&self) -> usize {
0
}
}
#[derive(Debug)]
pub struct TextColumn {
text: String,
style: Style,
}
impl TextColumn {
pub fn new(text: &str) -> Self {
Self {
text: text.to_string(),
style: Style::new(),
}
}
pub fn styled(text: &str, style: Style) -> Self {
Self {
text: text.to_string(),
style,
}
}
}
impl ProgressColumn for TextColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let text = if self.text == "[progress.description]" {
&task.description
} else {
&self.text
};
vec![Span::styled(text.clone(), self.style)]
}
}
#[derive(Debug)]
pub struct BarColumn {
pub bar_width: usize,
pub complete_char: char,
pub incomplete_char: char,
pub edge_char: Option<char>,
pub complete_style: Style,
pub finished_style: Option<Style>,
pub incomplete_style: Style,
pub pulse_style: Style,
pub expand: bool,
pub use_sub_blocks: bool,
}
impl Default for BarColumn {
fn default() -> Self {
Self::new(40)
}
}
impl BarColumn {
pub fn new(bar_width: usize) -> Self {
Self {
bar_width,
complete_char: '━', incomplete_char: '─', edge_char: Some('╸'), complete_style: Style::new().foreground(Color::Magenta),
finished_style: Some(Style::new().foreground(Color::Green)),
incomplete_style: Style::new().foreground(Color::Ansi256(237)), pulse_style: Style::new().foreground(Color::Cyan),
expand: false,
use_sub_blocks: false,
}
}
pub fn complete_char(mut self, c: char) -> Self {
self.complete_char = c;
self
}
pub fn incomplete_char(mut self, c: char) -> Self {
self.incomplete_char = c;
self
}
pub fn edge_char(mut self, c: Option<char>) -> Self {
self.edge_char = c;
self
}
pub fn complete_style(mut self, style: Style) -> Self {
self.complete_style = style;
self
}
pub fn finished_style(mut self, style: Option<Style>) -> Self {
self.finished_style = style;
self
}
pub fn expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
pub fn use_sub_blocks(mut self, enabled: bool) -> Self {
self.use_sub_blocks = enabled;
self
}
fn sub_block_char(eighths: usize) -> char {
match eighths {
0 => ' ', 1 => '▏', 2 => '▎', 3 => '▍', 4 => '▌', 5 => '▋', 6 => '▊', 7 => '▉', _ => '█', }
}
fn render_bar(&self, task: &Task, width: usize) -> Vec<Span> {
if task.total.is_none() && !task.finished {
return self.render_pulse_with_width(task, width);
}
let total = task.total.unwrap_or(100) as f64;
let completed = task.completed as f64;
let percentage = (completed / total).clamp(0.0, 1.0);
let style = if task.finished {
self.finished_style.unwrap_or(self.complete_style)
} else {
self.complete_style
};
if self.use_sub_blocks && !task.finished && percentage < 1.0 {
return self.render_sub_blocks(percentage, width, style);
}
let has_edge = self.edge_char.is_some() && !task.finished && percentage < 1.0;
let effective_width = if has_edge {
width.saturating_sub(1)
} else {
width
};
let filled_width = (effective_width as f64 * percentage).round() as usize;
let empty_width = effective_width.saturating_sub(filled_width);
let mut spans = Vec::new();
if filled_width > 0 {
spans.push(Span::styled(
self.complete_char.to_string().repeat(filled_width),
style,
));
}
if has_edge && filled_width < width {
if let Some(edge) = self.edge_char {
spans.push(Span::styled(edge.to_string(), style));
}
}
if empty_width > 0 {
spans.push(Span::styled(
self.incomplete_char.to_string().repeat(empty_width),
self.incomplete_style,
));
}
spans
}
fn render_sub_blocks(&self, percentage: f64, width: usize, style: Style) -> Vec<Span> {
let exact_filled = width as f64 * percentage;
let full_blocks = exact_filled as usize;
let fraction = exact_filled - full_blocks as f64;
let eighths = (fraction * 8.0).round() as usize;
let mut spans = Vec::new();
if full_blocks > 0 {
spans.push(Span::styled("█".repeat(full_blocks), style));
}
if eighths > 0 && full_blocks < width {
spans.push(Span::styled(
Self::sub_block_char(eighths).to_string(),
style,
));
}
let used_width = full_blocks + if eighths > 0 { 1 } else { 0 };
let empty_width = width.saturating_sub(used_width);
if empty_width > 0 {
spans.push(Span::styled(" ".repeat(empty_width), self.incomplete_style));
}
spans
}
fn render_pulse_with_width(&self, task: &Task, width: usize) -> Vec<Span> {
let pulse_width = 6.min(width / 3);
let elapsed_ms = task.elapsed().as_millis() as usize;
let cycle_duration_ms = 1500;
let position_in_cycle = elapsed_ms % cycle_duration_ms;
let half_cycle = cycle_duration_ms / 2;
let normalized_pos = if position_in_cycle < half_cycle {
position_in_cycle as f64 / half_cycle as f64
} else {
1.0 - ((position_in_cycle - half_cycle) as f64 / half_cycle as f64)
};
let pulse_start =
((width.saturating_sub(pulse_width)) as f64 * normalized_pos).round() as usize;
let pulse_end = pulse_start + pulse_width;
let mut spans = Vec::new();
if pulse_start > 0 {
spans.push(Span::styled(
self.incomplete_char.to_string().repeat(pulse_start),
self.incomplete_style,
));
}
spans.push(Span::styled(
self.complete_char.to_string().repeat(pulse_width),
self.pulse_style,
));
let after_pulse = width.saturating_sub(pulse_end);
if after_pulse > 0 {
spans.push(Span::styled(
self.incomplete_char.to_string().repeat(after_pulse),
self.incomplete_style,
));
}
spans
}
}
impl ProgressColumn for BarColumn {
fn render(&self, task: &Task) -> Vec<Span> {
self.render_bar(task, self.bar_width)
}
fn render_with_width(&self, task: &Task, available_width: Option<usize>) -> Vec<Span> {
let width = if self.expand {
available_width
.unwrap_or(self.bar_width)
.max(self.bar_width)
} else {
self.bar_width
};
self.render_bar(task, width)
}
fn is_expandable(&self) -> bool {
self.expand
}
fn min_width(&self) -> usize {
self.bar_width
}
}
#[derive(Debug)]
pub struct PercentageColumn(pub Style);
impl Default for PercentageColumn {
fn default() -> Self {
Self::new()
}
}
impl PercentageColumn {
pub fn new() -> Self {
Self(Style::new().foreground(Color::Cyan))
}
}
impl ProgressColumn for PercentageColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let percentage = task.percentage() * 100.0;
vec![Span::styled(format!("{:>3.0}%", percentage), self.0)]
}
}
#[derive(Debug)]
pub struct SpinnerColumn {
spinner: Spinner, }
impl Default for SpinnerColumn {
fn default() -> Self {
Self::new()
}
}
impl SpinnerColumn {
pub fn new() -> Self {
Self {
spinner: Spinner::new("").style(SpinnerStyle::Dots),
}
}
pub fn with_style(mut self, style: SpinnerStyle) -> Self {
self.spinner = Spinner::new("").style(style);
self
}
}
impl ProgressColumn for SpinnerColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let style = self.spinner.get_style();
let interval = style.interval_ms();
let frames = style.frames();
let elapsed_ms = task.elapsed().as_millis() as u64;
let idx = ((elapsed_ms / interval) as usize) % frames.len();
vec![Span::styled(
frames[idx].to_string(),
Style::new().foreground(Color::Green),
)]
}
}
#[derive(Debug)]
pub struct TransferSpeedColumn;
impl ProgressColumn for TransferSpeedColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let speed = task.speed();
let speed_str = if speed >= 1_000_000.0 {
format!("{:.1} MB/s", speed / 1_000_000.0)
} else if speed >= 1_000.0 {
format!("{:.1} KB/s", speed / 1_000.0)
} else {
format!("{:.0} B/s", speed)
};
vec![Span::styled(speed_str, Style::new().foreground(Color::Red))]
}
}
#[derive(Debug)]
pub struct TimeRemainingColumn;
impl ProgressColumn for TimeRemainingColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let eta = match task.eta() {
Some(d) => format_duration(d),
None => "-:--:--".to_string(),
};
vec![Span::styled(eta, Style::new().foreground(Color::Cyan))]
}
}
fn format_duration(d: Duration) -> String {
let secs = d.as_secs();
if secs >= 3600 {
format!(
"{:02}:{:02}:{:02}",
secs / 3600,
(secs % 3600) / 60,
secs % 60
)
} else {
format!("{:02}:{:02}", secs / 60, secs % 60)
}
}
#[derive(Debug)]
pub struct MofNColumn {
separator: String,
}
impl Default for MofNColumn {
fn default() -> Self {
Self::new()
}
}
impl MofNColumn {
pub fn new() -> Self {
Self {
separator: "/".to_string(),
}
}
}
impl ProgressColumn for MofNColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let completed = task.completed;
let total = task.total.unwrap_or(0);
vec![Span::styled(
format!("{}{}{}", completed, self.separator, total),
Style::new().foreground(Color::Green),
)]
}
}
#[derive(Debug)]
pub struct ElapsedColumn;
impl ProgressColumn for ElapsedColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let elapsed = task.elapsed();
vec![Span::styled(
format_duration(elapsed),
Style::new().foreground(Color::Cyan),
)]
}
}
fn format_bytes(bytes: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = 1024.0 * 1024.0;
const GB: f64 = 1024.0 * 1024.0 * 1024.0;
let bytes_f = bytes as f64;
if bytes_f >= GB {
format!("{:.1} GB", bytes_f / GB)
} else if bytes_f >= MB {
format!("{:.1} MB", bytes_f / MB)
} else if bytes_f >= KB {
format!("{:.1} KB", bytes_f / KB)
} else {
format!("{} B", bytes)
}
}
#[derive(Debug)]
pub struct FileSizeColumn {
style: Style,
}
impl Default for FileSizeColumn {
fn default() -> Self {
Self::new()
}
}
impl FileSizeColumn {
pub fn new() -> Self {
Self {
style: Style::new().foreground(Color::Green),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl ProgressColumn for FileSizeColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let size_str = format_bytes(task.completed);
vec![Span::styled(size_str, self.style)]
}
}
#[derive(Debug)]
pub struct TotalFileSizeColumn {
separator: String,
completed_style: Style,
total_style: Style,
}
impl Default for TotalFileSizeColumn {
fn default() -> Self {
Self::new()
}
}
impl TotalFileSizeColumn {
pub fn new() -> Self {
Self {
separator: " / ".to_string(),
completed_style: Style::new().foreground(Color::Green),
total_style: Style::new().foreground(Color::Blue),
}
}
pub fn separator(mut self, sep: &str) -> Self {
self.separator = sep.to_string();
self
}
pub fn completed_style(mut self, style: Style) -> Self {
self.completed_style = style;
self
}
pub fn total_style(mut self, style: Style) -> Self {
self.total_style = style;
self
}
}
impl ProgressColumn for TotalFileSizeColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let completed_str = format_bytes(task.completed);
let total_str = match task.total {
Some(t) => format_bytes(t),
None => "?".to_string(),
};
vec![
Span::styled(completed_str, self.completed_style),
Span::styled(self.separator.clone(), Style::new()),
Span::styled(total_str, self.total_style),
]
}
}
#[derive(Debug)]
pub struct DownloadColumn {
size_style: Style,
speed_style: Style,
}
impl Default for DownloadColumn {
fn default() -> Self {
Self::new()
}
}
impl DownloadColumn {
pub fn new() -> Self {
Self {
size_style: Style::new().foreground(Color::Green),
speed_style: Style::new().foreground(Color::Red),
}
}
pub fn size_style(mut self, style: Style) -> Self {
self.size_style = style;
self
}
pub fn speed_style(mut self, style: Style) -> Self {
self.speed_style = style;
self
}
}
impl ProgressColumn for DownloadColumn {
fn render(&self, task: &Task) -> Vec<Span> {
let size_str = format_bytes(task.completed);
let speed = task.speed();
let speed_str = if speed >= 1_000_000.0 {
format!("{:.1} MB/s", speed / 1_000_000.0)
} else if speed >= 1_000.0 {
format!("{:.1} KB/s", speed / 1_000.0)
} else {
format!("{:.0} B/s", speed)
};
vec![
Span::styled(size_str, self.size_style),
Span::raw(" @ "),
Span::styled(speed_str, self.speed_style),
]
}
}