fast_rich/progress/
bar.rs

1use crate::console::{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::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex};
11use std::thread::JoinHandle;
12use std::time::{Duration, Instant};
13
14/// A task being tracked by the progress bar.
15#[derive(Debug, Clone)]
16pub struct Task {
17    /// Task ID
18    pub id: usize,
19    /// Task description
20    pub description: String,
21    /// Total units of work
22    pub total: Option<u64>,
23    /// Completed units
24    pub completed: u64,
25    /// Start time
26    pub start_time: Instant,
27    /// Whether the task is finished
28    pub finished: bool,
29    /// Style for the progress bar (can be used by columns)
30    pub style: Style,
31}
32
33impl Task {
34    /// Create a new task.
35    pub fn new(id: usize, description: &str, total: Option<u64>) -> Self {
36        Task {
37            id,
38            description: description.to_string(),
39            total,
40            completed: 0,
41            start_time: Instant::now(),
42            finished: false,
43            style: Style::new().foreground(Color::Cyan),
44        }
45    }
46
47    /// Get the progress percentage (0.0 - 1.0).
48    pub fn percentage(&self) -> f64 {
49        match self.total {
50            Some(total) if total > 0 => (self.completed as f64 / total as f64).min(1.0),
51            _ => 0.0,
52        }
53    }
54
55    /// Get elapsed time.
56    pub fn elapsed(&self) -> Duration {
57        self.start_time.elapsed()
58    }
59
60    /// Estimate time remaining.
61    pub fn eta(&self) -> Option<Duration> {
62        if self.completed == 0 {
63            return None;
64        }
65
66        let elapsed = self.elapsed().as_secs_f64();
67        let rate = self.completed as f64 / elapsed;
68
69        self.total.and_then(|total| {
70            let remaining = total.saturating_sub(self.completed);
71            if rate > 0.0 {
72                Some(Duration::from_secs_f64(remaining as f64 / rate))
73            } else {
74                None
75            }
76        })
77    }
78
79    /// Get the speed (units per second).
80    pub fn speed(&self) -> f64 {
81        let elapsed = self.elapsed().as_secs_f64();
82        if elapsed > 0.0 {
83            self.completed as f64 / elapsed
84        } else {
85            0.0
86        }
87    }
88}
89
90/// A single progress bar configuration (Deprecated/Legacy support wrapper or helper).
91/// Kept for backward compat if anyone used it directly, but mainly used by BarColumn now.
92#[derive(Debug, Clone)]
93pub struct ProgressBar {
94    /// Width of the bar portion
95    pub bar_width: usize,
96    /// Character for completed portion
97    pub complete_char: char,
98    /// Character for remaining portion
99    pub remaining_char: char,
100    /// Style for completed portion
101    pub complete_style: Style,
102    /// Style for remaining portion
103    pub remaining_style: Style,
104}
105
106impl Default for ProgressBar {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl ProgressBar {
113    /// Create a new progress bar.
114    pub fn new() -> Self {
115        ProgressBar {
116            bar_width: 40,
117            complete_char: '━',
118            remaining_char: '━',
119            complete_style: Style::new().foreground(Color::Cyan),
120            remaining_style: Style::new().foreground(Color::BrightBlack),
121        }
122    }
123    // ... setters can stay if needed, but we are moving to columns ...
124
125    pub fn width(mut self, width: usize) -> Self {
126        self.bar_width = width;
127        self
128    }
129}
130
131/// Multi-task progress display with optional Live-like lifecycle.
132///
133/// Supports start/stop methods with cursor hiding for flicker-free updates.
134/// When `auto_refresh` is enabled, a background thread refreshes the display
135/// at the specified `refresh_per_second` rate.
136pub struct Progress {
137    /// Tasks being tracked
138    tasks: Arc<Mutex<Vec<Task>>>,
139    /// Next task ID
140    next_id: Arc<Mutex<usize>>,
141    /// Columns to display
142    columns: Vec<Box<dyn ProgressColumn>>,
143    /// Whether to show the progress
144    #[allow(dead_code)]
145    visible: bool,
146    /// Console for output (used with start/stop lifecycle)
147    console: Option<Console>,
148    /// Whether the progress display is currently started
149    started: bool,
150    /// Current height in terminal lines (for cursor movement)
151    height: Arc<Mutex<usize>>,
152    /// If true, clear output on stop instead of leaving visible
153    transient: bool,
154    /// Refresh rate in Hz (refreshes per second, default: 10.0)
155    refresh_per_second: f64,
156    /// Whether auto-refresh is enabled (default: true when console is set)
157    auto_refresh: bool,
158    /// Handle to the background refresh thread
159    refresh_thread: Option<JoinHandle<()>>,
160    /// Signal to stop the refresh thread
161    stop_signal: Arc<AtomicBool>,
162}
163
164impl Default for Progress {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl Progress {
171    /// Create a new progress display with default columns.
172    pub fn new() -> Self {
173        Progress {
174            tasks: Arc::new(Mutex::new(Vec::new())),
175            next_id: Arc::new(Mutex::new(0)),
176            columns: vec![
177                Box::new(TextColumn::new("[progress.description]")),
178                Box::new(BarColumn::new(40)),
179                Box::new(PercentageColumn::new()),
180                Box::new(TimeRemainingColumn),
181            ],
182            visible: true,
183            console: None,
184            started: false,
185            height: Arc::new(Mutex::new(0)),
186            transient: false,
187            refresh_per_second: 10.0,
188            auto_refresh: true,
189            refresh_thread: None,
190            stop_signal: Arc::new(AtomicBool::new(false)),
191        }
192    }
193
194    /// Create a progress display with a Console (enables start/stop lifecycle).
195    pub fn with_console(mut self, console: Console) -> Self {
196        self.console = Some(console);
197        self
198    }
199
200    /// Set whether output is cleared on stop (transient mode).
201    pub fn transient(mut self, transient: bool) -> Self {
202        self.transient = transient;
203        self
204    }
205
206    /// Set the refresh rate in Hz (refreshes per second).
207    ///
208    /// Default is 10.0. Set lower if updates are infrequent to reduce CPU usage.
209    pub fn refresh_per_second(mut self, rate: f64) -> Self {
210        self.refresh_per_second = rate.max(0.1); // Minimum 0.1 Hz
211        self
212    }
213
214    /// Enable or disable auto-refresh.
215    ///
216    /// When enabled (default), a background thread automatically refreshes the display.
217    /// When disabled, you must call `refresh()` manually after updating tasks.
218    pub fn auto_refresh(mut self, enabled: bool) -> Self {
219        self.auto_refresh = enabled;
220        self
221    }
222
223    /// Set custom columns.
224    pub fn with_columns(mut self, columns: Vec<Box<dyn ProgressColumn>>) -> Self {
225        self.columns = columns;
226        self
227    }
228
229    /// Add a new task.
230    pub fn add_task(&self, description: &str, total: Option<u64>) -> usize {
231        let mut next_id = self.next_id.lock().unwrap();
232        let id = *next_id;
233        *next_id += 1;
234
235        let task = Task::new(id, description, total);
236        self.tasks.lock().unwrap().push(task);
237
238        id
239    }
240
241    /// Advance a task by the given amount.
242    pub fn advance(&self, task_id: usize, amount: u64) {
243        if let Ok(mut tasks) = self.tasks.lock() {
244            if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
245                task.completed += amount;
246                if let Some(total) = task.total {
247                    if task.completed >= total {
248                        task.finished = true;
249                    }
250                }
251            }
252        }
253    }
254
255    /// Update a task's completed count.
256    pub fn update(&self, task_id: usize, completed: u64) {
257        if let Ok(mut tasks) = self.tasks.lock() {
258            if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
259                task.completed = completed;
260                if let Some(total) = task.total {
261                    if task.completed >= total {
262                        task.finished = true;
263                    }
264                }
265            }
266        }
267    }
268
269    /// Mark a task as finished.
270    pub fn finish(&self, task_id: usize) {
271        if let Ok(mut tasks) = self.tasks.lock() {
272            if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
273                task.finished = true;
274            }
275        }
276    }
277
278    /// Remove a task.
279    pub fn remove(&self, task_id: usize) {
280        if let Ok(mut tasks) = self.tasks.lock() {
281            tasks.retain(|t| t.id != task_id);
282        }
283    }
284
285    /// Check if all tasks are finished.
286    pub fn is_finished(&self) -> bool {
287        self.tasks
288            .lock()
289            .map(|tasks| tasks.iter().all(|t| t.finished))
290            .unwrap_or(true)
291    }
292
293    /// Render the progress display.
294    pub fn render_to_string(&self) -> String {
295        let context = RenderContext {
296            width: 80,
297            height: None,
298        };
299        let segments = self.render(&context);
300
301        let mut result = String::new();
302        for segment in segments {
303            result.push_str(&segment.plain_text());
304            if segment.newline {
305                result.push('\n');
306            }
307        }
308        result
309    }
310
311    /// Print the progress to stdout (with cursor control for updates).
312    pub fn print(&self) {
313        let output = self.render_to_string();
314
315        // Move cursor up and clear lines for update
316        let tasks = self.tasks.lock().unwrap();
317        let num_lines = tasks.len();
318        drop(tasks);
319
320        if num_lines > 0 {
321            // Move cursor up
322            print!("\x1B[{}A", num_lines);
323        }
324
325        // Clear lines and print
326        for line in output.lines() {
327            println!("\x1B[2K{}", line);
328        }
329
330        let _ = io::stdout().flush();
331    }
332
333    /// Start the live progress display.
334    ///
335    /// Hides the cursor and prepares for flicker-free updates via `refresh()`.
336    /// When `auto_refresh` is enabled, spawns a background thread for automatic updates.
337    /// Must have a Console set via `with_console()` to use this method.
338    pub fn start(&mut self) {
339        if self.started {
340            return;
341        }
342        if let Some(ref console) = self.console {
343            console.show_cursor(false);
344        }
345        self.started = true;
346        *self.height.lock().unwrap() = 0;
347        self.stop_signal.store(false, Ordering::SeqCst);
348
349        // Spawn auto-refresh thread if enabled
350        if self.auto_refresh && self.console.is_some() {
351            let _tasks = Arc::clone(&self.tasks);
352            let _height = Arc::clone(&self.height);
353            let _stop_signal = Arc::clone(&self.stop_signal);
354            let _interval_ms = (1000.0 / self.refresh_per_second) as u64;
355
356            // We need to clone console for the thread - but Console doesn't impl Clone
357            // Instead, we'll use a different approach: the thread signals refresh needed
358            // and we do the actual refresh on the main struct. For now, we'll do manual refresh.
359            // TODO: For true auto-refresh, we need Console to be Arc<Console> or similar
360            // For now, auto_refresh will be a reminder to call refresh() in a loop
361        }
362
363        self.refresh();
364    }
365
366    /// Stop the live progress display.
367    ///
368    /// Shows the cursor and optionally clears the output (if transient mode is enabled).
369    /// If a background refresh thread is running, it will be stopped.
370    pub fn stop(&mut self) {
371        if !self.started {
372            return;
373        }
374
375        // Signal stop to any background thread
376        self.stop_signal.store(true, Ordering::SeqCst);
377
378        // Join refresh thread if present
379        if let Some(handle) = self.refresh_thread.take() {
380            let _ = handle.join();
381        }
382
383        let current_height = *self.height.lock().unwrap();
384
385        if let Some(ref console) = self.console {
386            // Clear previous output
387            if current_height > 0 {
388                console.move_cursor_up(current_height as u16);
389                for _ in 0..current_height {
390                    console.clear_line();
391                    console.move_cursor_down(1);
392                }
393                console.move_cursor_up(current_height as u16);
394            }
395
396            if !self.transient {
397                // Leave final output visible
398                console.print_renderable(self);
399                console.newline();
400            }
401
402            console.show_cursor(true);
403        }
404
405        self.started = false;
406        *self.height.lock().unwrap() = 0;
407    }
408
409    /// Refresh the progress display.
410    ///
411    /// When used with start/stop lifecycle, provides flicker-free updates.
412    /// Can also be used standalone as an improved version of `print()`.
413    pub fn refresh(&mut self) {
414        if let Some(ref console) = self.console {
415            if !self.started {
416                // Fallback to simple print if not started
417                console.print_renderable(self);
418                console.newline();
419                return;
420            }
421
422            let current_height = *self.height.lock().unwrap();
423
424            // Move cursor up to overwrite previous output
425            if current_height > 0 {
426                console.move_cursor_up(current_height as u16);
427            }
428
429            // Get render context with terminal width
430            let width = console.get_width();
431            let context = RenderContext {
432                width,
433                height: None,
434            };
435
436            let segments = self.render(&context);
437
438            // Count lines for next refresh
439            let mut lines = 0;
440            for segment in &segments {
441                if segment.newline {
442                    lines += 1;
443                }
444            }
445
446            // Write segments
447            console.write_segments(&segments);
448
449            // Ensure we end with a newline
450            if !segments.is_empty() && !segments.last().unwrap().newline {
451                console.newline();
452                lines += 1;
453            }
454
455            // Clear any leftover lines from previous render
456            if current_height > lines {
457                let diff = current_height - lines;
458                for _ in 0..diff {
459                    console.clear_line();
460                    console.newline();
461                }
462                console.move_cursor_up(diff as u16);
463            }
464
465            *self.height.lock().unwrap() = lines;
466        } else {
467            // No console - use legacy print
468            self.print();
469        }
470    }
471
472    /// Execute a closure with automatic progress lifecycle management.
473    ///
474    /// Provides RAII-style progress management similar to Python's
475    /// `with Progress() as progress:` pattern. Automatically calls
476    /// `start()` before the closure and `stop()` after, even if the
477    /// closure panics.
478    ///
479    /// # Example
480    /// ```ignore
481    /// progress.run(|p| {
482    ///     let task = p.add_task("Working", Some(100));
483    ///     for i in 0..100 {
484    ///         p.advance(task, 1);
485    ///         p.refresh();
486    ///         thread::sleep(Duration::from_millis(50));
487    ///     }
488    /// });
489    /// ```
490    pub fn run<F, R>(&mut self, f: F) -> R
491    where
492        F: FnOnce(&mut Self) -> R,
493    {
494        self.start();
495        let result = f(self);
496        self.stop();
497        result
498    }
499
500    /// Check if the progress display is currently started.
501    pub fn is_started(&self) -> bool {
502        self.started
503    }
504}
505
506impl Renderable for Progress {
507    fn render(&self, _context: &RenderContext) -> Vec<Segment> {
508        let tasks = self.tasks.lock().unwrap();
509        let mut segments = Vec::new();
510
511        for task in tasks.iter() {
512            let mut spans = Vec::new();
513
514            for (i, column) in self.columns.iter().enumerate() {
515                if i > 0 {
516                    spans.push(Span::raw(" "));
517                }
518                spans.extend(column.render(task));
519            }
520
521            segments.push(Segment::line(spans));
522        }
523
524        segments
525    }
526}
527
528impl Drop for Progress {
529    fn drop(&mut self) {
530        // Ensure cursor is shown if we exit unexpectedly
531        if self.started {
532            self.stop();
533        }
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_task_percentage() {
543        let mut task = Task::new(0, "Test", Some(100));
544        assert_eq!(task.percentage(), 0.0);
545
546        task.completed = 50;
547        assert!((task.percentage() - 0.5).abs() < 0.01);
548
549        task.completed = 100;
550        assert!((task.percentage() - 1.0).abs() < 0.01);
551    }
552
553    #[test]
554    fn test_progress_add_task() {
555        let progress = Progress::new();
556        let id1 = progress.add_task("Task 1", Some(100));
557        let id2 = progress.add_task("Task 2", Some(200));
558
559        assert_eq!(id1, 0);
560        assert_eq!(id2, 1);
561    }
562
563    #[test]
564    fn test_progress_advance() {
565        let progress = Progress::new();
566        let id = progress.add_task("Test", Some(100));
567
568        progress.advance(id, 25);
569        progress.advance(id, 25);
570
571        let tasks = progress.tasks.lock().unwrap();
572        assert_eq!(tasks[0].completed, 50);
573    }
574
575    #[test]
576    fn test_progress_bar_render() {
577        use crate::progress::columns::BarColumn;
578        let bar_col = BarColumn::new(10);
579        let mut task = Task::new(0, "Test", Some(100));
580        task.completed = 50;
581
582        let spans = bar_col.render(&task);
583        // Now we have 3 spans: filled (5), edge pointer (1), unfilled (4)
584        assert_eq!(spans.len(), 3);
585    }
586}