fast_rich/progress/
bar.rs

1use crate::console::RenderContext;
2use crate::progress::columns::{BarColumn, PercentageColumn, ProgressColumn, TextColumn, TimeRemainingColumn};
3use crate::renderable::{Renderable, Segment};
4use crate::style::{Color, Style};
5use crate::text::Span;
6use std::io::{self, Write};
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10/// A task being tracked by the progress bar.
11#[derive(Debug, Clone)]
12pub struct Task {
13    /// Task ID
14    pub id: usize,
15    /// Task description
16    pub description: String,
17    /// Total units of work
18    pub total: Option<u64>,
19    /// Completed units
20    pub completed: u64,
21    /// Start time
22    pub start_time: Instant,
23    /// Whether the task is finished
24    pub finished: bool,
25    /// Style for the progress bar (can be used by columns)
26    pub style: Style,
27}
28
29impl Task {
30    /// Create a new task.
31    pub fn new(id: usize, description: &str, total: Option<u64>) -> Self {
32        Task {
33            id,
34            description: description.to_string(),
35            total,
36            completed: 0,
37            start_time: Instant::now(),
38            finished: false,
39            style: Style::new().foreground(Color::Cyan),
40        }
41    }
42
43    /// Get the progress percentage (0.0 - 1.0).
44    pub fn percentage(&self) -> f64 {
45        match self.total {
46            Some(total) if total > 0 => (self.completed as f64 / total as f64).min(1.0),
47            _ => 0.0,
48        }
49    }
50
51    /// Get elapsed time.
52    pub fn elapsed(&self) -> Duration {
53        self.start_time.elapsed()
54    }
55
56    /// Estimate time remaining.
57    pub fn eta(&self) -> Option<Duration> {
58        if self.completed == 0 {
59            return None;
60        }
61
62        let elapsed = self.elapsed().as_secs_f64();
63        let rate = self.completed as f64 / elapsed;
64
65        self.total.and_then(|total| {
66            let remaining = total.saturating_sub(self.completed);
67            if rate > 0.0 {
68                Some(Duration::from_secs_f64(remaining as f64 / rate))
69            } else {
70                None
71            }
72        })
73    }
74
75    /// Get the speed (units per second).
76    pub fn speed(&self) -> f64 {
77        let elapsed = self.elapsed().as_secs_f64();
78        if elapsed > 0.0 {
79            self.completed as f64 / elapsed
80        } else {
81            0.0
82        }
83    }
84}
85
86/// A single progress bar configuration (Deprecated/Legacy support wrapper or helper).
87/// Kept for backward compat if anyone used it directly, but mainly used by BarColumn now.
88#[derive(Debug, Clone)]
89pub struct ProgressBar {
90    /// Width of the bar portion
91    pub bar_width: usize,
92    /// Character for completed portion
93    pub complete_char: char,
94    /// Character for remaining portion
95    pub remaining_char: char,
96    /// Style for completed portion
97    pub complete_style: Style,
98    /// Style for remaining portion
99    pub remaining_style: Style,
100}
101
102impl Default for ProgressBar {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl ProgressBar {
109    /// Create a new progress bar.
110    pub fn new() -> Self {
111        ProgressBar {
112            bar_width: 40,
113            complete_char: '━',
114            remaining_char: '━',
115            complete_style: Style::new().foreground(Color::Cyan),
116            remaining_style: Style::new().foreground(Color::BrightBlack),
117        }
118    }
119    // ... setters can stay if needed, but we are moving to columns ...
120    
121    pub fn width(mut self, width: usize) -> Self {
122        self.bar_width = width;
123        self
124    }
125}
126
127/// Multi-task progress display.
128#[derive(Debug)]
129pub struct Progress {
130    /// Tasks being tracked
131    tasks: Arc<Mutex<Vec<Task>>>,
132    /// Next task ID
133    next_id: Arc<Mutex<usize>>,
134    /// Columns to display
135    columns: Vec<Box<dyn ProgressColumn>>,
136    /// Whether to show the progress
137    #[allow(dead_code)]
138    visible: bool,
139    /// Refresh rate in milliseconds
140    #[allow(dead_code)]
141    refresh_rate_ms: u64,
142}
143
144impl Default for Progress {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl Progress {
151    /// Create a new progress display with default columns.
152    pub fn new() -> Self {
153        Progress {
154            tasks: Arc::new(Mutex::new(Vec::new())),
155            next_id: Arc::new(Mutex::new(0)),
156            columns: vec![
157                Box::new(TextColumn::new("[progress.description]")),
158                Box::new(BarColumn::new(40)),
159                Box::new(PercentageColumn::new()),
160                Box::new(TimeRemainingColumn),
161            ],
162            visible: true,
163            refresh_rate_ms: 100,
164        }
165    }
166
167    /// Set custom columns.
168    pub fn with_columns(mut self, columns: Vec<Box<dyn ProgressColumn>>) -> Self {
169        self.columns = columns;
170        self
171    }
172
173    /// Add a new task.
174    pub fn add_task(&self, description: &str, total: Option<u64>) -> usize {
175        let mut next_id = self.next_id.lock().unwrap();
176        let id = *next_id;
177        *next_id += 1;
178
179        let task = Task::new(id, description, total);
180        self.tasks.lock().unwrap().push(task);
181
182        id
183    }
184
185    /// Advance a task by the given amount.
186    pub fn advance(&self, task_id: usize, amount: u64) {
187        if let Ok(mut tasks) = self.tasks.lock() {
188            if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
189                task.completed += amount;
190                if let Some(total) = task.total {
191                    if task.completed >= total {
192                        task.finished = true;
193                    }
194                }
195            }
196        }
197    }
198
199    /// Update a task's completed count.
200    pub fn update(&self, task_id: usize, completed: u64) {
201        if let Ok(mut tasks) = self.tasks.lock() {
202            if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
203                task.completed = completed;
204                if let Some(total) = task.total {
205                    if task.completed >= total {
206                        task.finished = true;
207                    }
208                }
209            }
210        }
211    }
212
213    /// Mark a task as finished.
214    pub fn finish(&self, task_id: usize) {
215        if let Ok(mut tasks) = self.tasks.lock() {
216            if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
217                task.finished = true;
218            }
219        }
220    }
221
222    /// Remove a task.
223    pub fn remove(&self, task_id: usize) {
224        if let Ok(mut tasks) = self.tasks.lock() {
225            tasks.retain(|t| t.id != task_id);
226        }
227    }
228
229    /// Check if all tasks are finished.
230    pub fn is_finished(&self) -> bool {
231        self.tasks
232            .lock()
233            .map(|tasks| tasks.iter().all(|t| t.finished))
234            .unwrap_or(true)
235    }
236
237    /// Render the progress display.
238    pub fn render_to_string(&self) -> String {
239        let context = RenderContext { width: 80, height: None };
240        let segments = self.render(&context);
241
242        let mut result = String::new();
243        for segment in segments {
244            result.push_str(&segment.plain_text());
245            if segment.newline {
246                result.push('\n');
247            }
248        }
249        result
250    }
251
252    /// Print the progress to stdout (with cursor control for updates).
253    pub fn print(&self) {
254        let output = self.render_to_string();
255
256        // Move cursor up and clear lines for update
257        let tasks = self.tasks.lock().unwrap();
258        let num_lines = tasks.len();
259        drop(tasks);
260
261        if num_lines > 0 {
262            // Move cursor up
263            print!("\x1B[{}A", num_lines);
264        }
265
266        // Clear lines and print
267        for line in output.lines() {
268            println!("\x1B[2K{}", line);
269        }
270
271        let _ = io::stdout().flush();
272    }
273}
274
275impl Renderable for Progress {
276    fn render(&self, _context: &RenderContext) -> Vec<Segment> {
277        let tasks = self.tasks.lock().unwrap();
278        let mut segments = Vec::new();
279
280        for task in tasks.iter() {
281            let mut spans = Vec::new();
282
283            for (i, column) in self.columns.iter().enumerate() {
284                if i > 0 {
285                    spans.push(Span::raw(" "));
286                }
287                spans.extend(column.render(task));
288            }
289
290            segments.push(Segment::line(spans));
291        }
292
293        segments
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_task_percentage() {
303        let mut task = Task::new(0, "Test", Some(100));
304        assert_eq!(task.percentage(), 0.0);
305
306        task.completed = 50;
307        assert!((task.percentage() - 0.5).abs() < 0.01);
308
309        task.completed = 100;
310        assert!((task.percentage() - 1.0).abs() < 0.01);
311    }
312
313    #[test]
314    fn test_progress_add_task() {
315        let progress = Progress::new();
316        let id1 = progress.add_task("Task 1", Some(100));
317        let id2 = progress.add_task("Task 2", Some(200));
318
319        assert_eq!(id1, 0);
320        assert_eq!(id2, 1);
321    }
322
323    #[test]
324    fn test_progress_advance() {
325        let progress = Progress::new();
326        let id = progress.add_task("Test", Some(100));
327
328        progress.advance(id, 25);
329        progress.advance(id, 25);
330
331        let tasks = progress.tasks.lock().unwrap();
332        assert_eq!(tasks[0].completed, 50);
333    }
334
335    #[test]
336    fn test_progress_bar_render() {
337        use crate::progress::columns::BarColumn;
338        let bar_col = BarColumn::new(10);
339        let mut task = Task::new(0, "Test", Some(100));
340        task.completed = 50;
341
342        let spans = bar_col.render(&task);
343        assert_eq!(spans.len(), 2);
344    }
345}