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("━".repeat(empty_width), Style::new().foreground(Color::Ansi256(237)))); // Grey
93        }
94        spans
95    }
96}
97
98/// Renders the percentage complete (e.g. "50%").
99#[derive(Debug)]
100pub struct PercentageColumn(pub Style);
101
102impl Default for PercentageColumn {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl PercentageColumn {
109    pub fn new() -> Self {
110        Self(Style::new().foreground(Color::Cyan))
111    }
112}
113
114impl ProgressColumn for PercentageColumn {
115    fn render(&self, task: &Task) -> Vec<Span> {
116        let percentage = task.percentage() * 100.0;
117        vec![Span::styled(format!("{:>3.0}%", percentage), self.0)]
118    }
119}
120
121/// Renders the spinner
122#[derive(Debug)]
123pub struct SpinnerColumn {
124    spinner: Spinner, // Use generic spinner for frames
125}
126
127impl Default for SpinnerColumn {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl SpinnerColumn {
134    pub fn new() -> Self {
135        Self {
136            spinner: Spinner::new("").style(SpinnerStyle::Dots),
137        }
138    }
139}
140
141impl ProgressColumn for SpinnerColumn {
142    fn render(&self, task: &Task) -> Vec<Span> {
143        // We use the task's elapsed time to calculate the frame
144        // This keeps it stateless with respect to the column, but animated by the task's lifetime.
145        // For a global spinner independent of task start, we might need a shared start time.
146        // But usually spinners in task rows indicate THAT task's activity.
147        
148        // However, generic Spinner uses its own start_time.
149        // We should probably rely on `SpinnerStyle` and manual calculation using task.elapsed()
150        // to avoid storing state that drifts.
151        
152        // Let's copy logic from Spinner::current_frame but use task.elapsed()
153        let style = self.spinner.get_style();
154        let interval = style.interval_ms();
155        let frames = style.frames();
156        let elapsed_ms = task.elapsed().as_millis() as u64;
157        let idx = ((elapsed_ms / interval) as usize) % frames.len();
158        
159        vec![Span::styled(frames[idx].to_string(), Style::new().foreground(Color::Green))]
160    }
161}
162
163/// Renders transfer speed
164#[derive(Debug)]
165pub struct TransferSpeedColumn;
166
167impl ProgressColumn for TransferSpeedColumn {
168    fn render(&self, task: &Task) -> Vec<Span> {
169        let speed = task.speed();
170         let speed_str = if speed >= 1_000_000.0 {
171            format!("{:.1} MB/s", speed / 1_000_000.0)
172        } else if speed >= 1_000.0 {
173            format!("{:.1} KB/s", speed / 1_000.0)
174        } else {
175             format!("{:.0} B/s", speed)
176        };
177        vec![Span::styled(speed_str, Style::new().foreground(Color::Red))]
178    }
179}
180
181/// Renders time remaining
182#[derive(Debug)]
183pub struct TimeRemainingColumn;
184
185impl ProgressColumn for TimeRemainingColumn {
186    fn render(&self, task: &Task) -> Vec<Span> {
187        let eta = match task.eta() {
188             Some(d) => format_duration(d),
189             None => "-:--:--".to_string(),
190        };
191        vec![Span::styled(eta, Style::new().foreground(Color::Cyan))] 
192    }
193}
194
195fn format_duration(d: Duration) -> String {
196    let secs = d.as_secs();
197    if secs >= 3600 {
198        format!("{:02}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
199    } else {
200        format!("{:02}:{:02}", secs / 60, secs % 60)
201    }
202}
203
204#[derive(Debug)]
205pub struct MofNColumn {
206    separator: String
207}
208
209impl Default for MofNColumn {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215impl MofNColumn {
216    pub fn new() -> Self {
217        Self { separator: "/".to_string() }
218    }
219}
220
221impl ProgressColumn for MofNColumn {
222    fn render(&self, task: &Task) -> Vec<Span> {
223        let completed = task.completed;
224        let total = task.total.unwrap_or(0);
225        vec![Span::styled(format!("{}{}{}", completed, self.separator, total), Style::new().foreground(Color::Green))]
226    }
227}
228
229#[derive(Debug)]
230pub struct ElapsedColumn;
231
232impl ProgressColumn for ElapsedColumn {
233    fn render(&self, task: &Task) -> Vec<Span> {
234        let elapsed = task.elapsed();
235        vec![Span::styled(format_duration(elapsed), Style::new().foreground(Color::Cyan))]
236    }
237}