fast_rich/progress/
bar.rs

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