fast_rich/progress/
columns.rs

1use crate::progress::Task;
2// use crate::progress::bar::ProgressBar;
3use crate::progress::spinner::{Spinner, SpinnerStyle};
4use crate::style::{Color, Style};
5use crate::text::Span;
6use std::fmt::Debug;
7use std::time::Duration;
8
9/// A trait for rendering a column in a progress bar.
10pub trait ProgressColumn: Send + Sync + Debug {
11    /// Render the column for the given task.
12    fn render(&self, task: &Task) -> Vec<Span>;
13}
14
15/// Renders a static text string or text based on task properties.
16#[derive(Debug)]
17pub struct TextColumn {
18    text: String,
19    style: Style,
20}
21
22impl TextColumn {
23    pub fn new(text: &str) -> Self {
24        Self {
25            text: text.to_string(),
26            style: Style::new(),
27        }
28    }
29
30    pub fn styled(text: &str, style: Style) -> Self {
31        Self {
32            text: text.to_string(),
33            style,
34        }
35    }
36}
37
38impl ProgressColumn for TextColumn {
39    fn render(&self, task: &Task) -> Vec<Span> {
40        // Simple interpolation for now task.description
41        let text = if self.text == "[progress.description]" {
42            &task.description
43        } else {
44            &self.text
45        };
46
47        vec![Span::styled(text.clone(), self.style)]
48    }
49}
50
51/// Renders the progress bar.
52#[derive(Debug)]
53pub struct BarColumn {
54    pub bar_width: usize,
55    pub complete_style: Style,
56    pub finished_style: Option<Style>,
57    pub pulse_style: Option<Style>,
58}
59
60impl BarColumn {
61    pub fn new(bar_width: usize) -> Self {
62        Self {
63            bar_width,
64            complete_style: Style::new().foreground(Color::Magenta), // Default rich color
65            finished_style: Some(Style::new().foreground(Color::Green)),
66            pulse_style: None,
67        }
68    }
69}
70
71impl ProgressColumn for BarColumn {
72    fn render(&self, task: &Task) -> Vec<Span> {
73        let total = task.total.unwrap_or(100) as f64;
74        let completed = task.completed as f64;
75        let percentage = (completed / total).clamp(0.0, 1.0);
76
77        let width = self.bar_width;
78        let filled_width = (width as f64 * percentage).round() as usize;
79        let empty_width = width.saturating_sub(filled_width);
80
81        let style = if task.finished {
82            self.finished_style.unwrap_or(self.complete_style)
83        } else {
84            self.complete_style
85        };
86
87        let mut spans = Vec::new();
88        if filled_width > 0 {
89            spans.push(Span::styled("━".repeat(filled_width), style));
90        }
91        if empty_width > 0 {
92            spans.push(Span::styled(
93                "━".repeat(empty_width),
94                Style::new().foreground(Color::Ansi256(237)),
95            )); // Grey
96        }
97        spans
98    }
99}
100
101/// Renders the percentage complete (e.g. "50%").
102#[derive(Debug)]
103pub struct PercentageColumn(pub Style);
104
105impl Default for PercentageColumn {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl PercentageColumn {
112    pub fn new() -> Self {
113        Self(Style::new().foreground(Color::Cyan))
114    }
115}
116
117impl ProgressColumn for PercentageColumn {
118    fn render(&self, task: &Task) -> Vec<Span> {
119        let percentage = task.percentage() * 100.0;
120        vec![Span::styled(format!("{:>3.0}%", percentage), self.0)]
121    }
122}
123
124/// Renders the spinner
125#[derive(Debug)]
126pub struct SpinnerColumn {
127    spinner: Spinner, // Use generic spinner for frames
128}
129
130impl Default for SpinnerColumn {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl SpinnerColumn {
137    pub fn new() -> Self {
138        Self {
139            spinner: Spinner::new("").style(SpinnerStyle::Dots),
140        }
141    }
142}
143
144impl ProgressColumn for SpinnerColumn {
145    fn render(&self, task: &Task) -> Vec<Span> {
146        // We use the task's elapsed time to calculate the frame
147        // This keeps it stateless with respect to the column, but animated by the task's lifetime.
148        // For a global spinner independent of task start, we might need a shared start time.
149        // But usually spinners in task rows indicate THAT task's activity.
150
151        // However, generic Spinner uses its own start_time.
152        // We should probably rely on `SpinnerStyle` and manual calculation using task.elapsed()
153        // to avoid storing state that drifts.
154
155        // Let's copy logic from Spinner::current_frame but use task.elapsed()
156        let style = self.spinner.get_style();
157        let interval = style.interval_ms();
158        let frames = style.frames();
159        let elapsed_ms = task.elapsed().as_millis() as u64;
160        let idx = ((elapsed_ms / interval) as usize) % frames.len();
161
162        vec![Span::styled(
163            frames[idx].to_string(),
164            Style::new().foreground(Color::Green),
165        )]
166    }
167}
168
169/// Renders transfer speed
170#[derive(Debug)]
171pub struct TransferSpeedColumn;
172
173impl ProgressColumn for TransferSpeedColumn {
174    fn render(&self, task: &Task) -> Vec<Span> {
175        let speed = task.speed();
176        let speed_str = if speed >= 1_000_000.0 {
177            format!("{:.1} MB/s", speed / 1_000_000.0)
178        } else if speed >= 1_000.0 {
179            format!("{:.1} KB/s", speed / 1_000.0)
180        } else {
181            format!("{:.0} B/s", speed)
182        };
183        vec![Span::styled(speed_str, Style::new().foreground(Color::Red))]
184    }
185}
186
187/// Renders time remaining
188#[derive(Debug)]
189pub struct TimeRemainingColumn;
190
191impl ProgressColumn for TimeRemainingColumn {
192    fn render(&self, task: &Task) -> Vec<Span> {
193        let eta = match task.eta() {
194            Some(d) => format_duration(d),
195            None => "-:--:--".to_string(),
196        };
197        vec![Span::styled(eta, Style::new().foreground(Color::Cyan))]
198    }
199}
200
201fn format_duration(d: Duration) -> String {
202    let secs = d.as_secs();
203    if secs >= 3600 {
204        format!(
205            "{:02}:{:02}:{:02}",
206            secs / 3600,
207            (secs % 3600) / 60,
208            secs % 60
209        )
210    } else {
211        format!("{:02}:{:02}", secs / 60, secs % 60)
212    }
213}
214
215#[derive(Debug)]
216pub struct MofNColumn {
217    separator: String,
218}
219
220impl Default for MofNColumn {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226impl MofNColumn {
227    pub fn new() -> Self {
228        Self {
229            separator: "/".to_string(),
230        }
231    }
232}
233
234impl ProgressColumn for MofNColumn {
235    fn render(&self, task: &Task) -> Vec<Span> {
236        let completed = task.completed;
237        let total = task.total.unwrap_or(0);
238        vec![Span::styled(
239            format!("{}{}{}", completed, self.separator, total),
240            Style::new().foreground(Color::Green),
241        )]
242    }
243}
244
245#[derive(Debug)]
246pub struct ElapsedColumn;
247
248impl ProgressColumn for ElapsedColumn {
249    fn render(&self, task: &Task) -> Vec<Span> {
250        let elapsed = task.elapsed();
251        vec![Span::styled(
252            format_duration(elapsed),
253            Style::new().foreground(Color::Cyan),
254        )]
255    }
256}