scirs2_core/logging/progress/
renderer.rs

1//! Progress bar renderer
2//!
3//! This module handles the terminal rendering of progress bars with different
4//! styles and formatting options.
5
6use std::io::{self, Write};
7
8use super::statistics::{format_duration, format_rate, ProgressStats};
9use super::tracker::ProgressSymbols;
10
11/// Progress bar renderer
12pub struct ProgressRenderer {
13    /// Terminal handling state
14    last_length: usize,
15    /// Spinner index for animated spinners
16    spinner_index: usize,
17}
18
19impl ProgressRenderer {
20    /// Create a new renderer
21    pub fn new() -> Self {
22        Self {
23            last_length: 0,
24            spinner_index: 0,
25        }
26    }
27
28    /// Initialize terminal for progress rendering
29    pub fn init(&mut self) {
30        // Hide cursor for cleaner progress display
31        print!("\x1b[?25l");
32        let _ = io::stdout().flush();
33    }
34
35    /// Finalize terminal after progress rendering
36    pub fn finalize(&mut self) {
37        // Show cursor and print a newline to ensure next output starts on a fresh line
38        print!("\x1b[?25h");
39        println!();
40        let _ = io::stdout().flush();
41    }
42
43    /// Render percentage-only progress
44    pub fn renderpercentage(&self, description: &str, stats: &ProgressStats) {
45        let output = format!(
46            "{description}: {percentage:.1}%",
47            percentage = stats.percentage
48        );
49        self.print_progress(&output);
50    }
51
52    /// Render basic progress bar
53    pub fn render_basic(
54        &self,
55        description: &str,
56        stats: &ProgressStats,
57        width: usize,
58        show_eta: bool,
59        symbols: &ProgressSymbols,
60    ) {
61        let percentage = stats.percentage;
62        let filled_width = ((percentage / 100.0) * width as f64) as usize;
63        let empty_width = width.saturating_sub(filled_width);
64
65        let progress_bar = format!(
66            "{}{}{}{}",
67            symbols.start,
68            symbols.fill.repeat(filled_width),
69            symbols.empty.repeat(empty_width),
70            symbols.end,
71        );
72
73        let mut output = format!("{description}: {progress_bar} {percentage:.1}%");
74
75        if show_eta && stats.processed < stats.total {
76            output.push_str(&format!(" eta: {}", format_duration(&stats.eta)));
77        }
78
79        self.print_progress(&output);
80    }
81
82    /// Render spinner progress
83    pub fn render_spinner(
84        &mut self,
85        description: &str,
86        stats: &ProgressStats,
87        show_eta: bool,
88        symbols: &ProgressSymbols,
89    ) {
90        self.spinner_index = (self.spinner_index + 1) % symbols.spinner.len();
91        let spinner = &symbols.spinner[self.spinner_index];
92
93        let mut output = format!(
94            "{} {} {}/{} ({:.1}%)",
95            spinner, description, stats.processed, stats.total, stats.percentage
96        );
97
98        if show_eta && stats.processed < stats.total {
99            output.push_str(&format!(" eta: {}", format_duration(&stats.eta)));
100        }
101
102        self.print_progress(&output);
103    }
104
105    /// Render detailed progress bar with statistics
106    pub fn render_detailed(
107        &self,
108        description: &str,
109        stats: &ProgressStats,
110        width: usize,
111        show_speed: bool,
112        show_eta: bool,
113        show_statistics: bool,
114        symbols: &ProgressSymbols,
115    ) {
116        let percentage = stats.percentage;
117        let filled_width = ((percentage / 100.0) * width as f64) as usize;
118        let empty_width = width.saturating_sub(filled_width);
119
120        let progress_bar = format!(
121            "{}{}{}{}",
122            symbols.start,
123            symbols.fill.repeat(filled_width),
124            symbols.empty.repeat(empty_width),
125            symbols.end,
126        );
127
128        let mut output = format!(
129            "{}: {} {:.1}% ({}/{})",
130            description, progress_bar, percentage, stats.processed, stats.total
131        );
132
133        if show_speed {
134            output.push_str(&format!(
135                " [{rate}]",
136                rate = format_rate(stats.items_per_second)
137            ));
138        }
139
140        if show_eta && stats.processed < stats.total {
141            output.push_str(&format!(" eta: {}", format_duration(&stats.eta)));
142        }
143
144        if show_statistics {
145            output.push_str(&format!(
146                " | Elapsed: {elapsed}",
147                elapsed = format_duration(&stats.elapsed)
148            ));
149
150            if stats.max_speed > 0.0 {
151                output.push_str(&format!(
152                    " | Peak: {peak}",
153                    peak = format_rate(stats.max_speed)
154                ));
155            }
156        }
157
158        self.print_progress(&output);
159    }
160
161    /// Render a compact progress display suitable for log files
162    pub fn render_compact(&self, description: &str, stats: &ProgressStats) {
163        let output = format!(
164            "{}: {}/{} ({:.1}%) - {} - ETA: {}",
165            description,
166            stats.processed,
167            stats.total,
168            stats.percentage,
169            format_rate(stats.items_per_second),
170            format_duration(&stats.eta)
171        );
172        self.print_progress(&output);
173    }
174
175    /// Print progress with carriage return for in-place updates
176    fn print_progress(&self, output: &str) {
177        // Calculate the display width (handling Unicode characters)
178        let output_width = console_width(output);
179
180        // Clear previous output if it was longer
181        if self.last_length > output_width {
182            let clear_length = self.last_length - output_width;
183            print!("\r{}{}", output, " ".repeat(clear_length));
184        } else {
185            print!("\r{output}");
186        }
187
188        let _ = io::stdout().flush();
189    }
190
191    /// Update the stored length for proper clearing
192    pub fn update_length(&mut self, length: usize) {
193        self.last_length = length;
194    }
195}
196
197impl Default for ProgressRenderer {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203/// Calculate the display width of a string, accounting for Unicode
204#[allow(dead_code)]
205fn console_width(s: &str) -> usize {
206    // This is a simplified implementation
207    // For full Unicode support, you might want to use the `unicode-width` crate
208    s.chars().count()
209}
210
211/// Generate a color-coded progress bar based on completion percentage
212#[allow(dead_code)]
213pub fn colored_progress_bar(percentage: f64, width: usize) -> String {
214    let filled_width = ((percentage / 100.0) * width as f64) as usize;
215    let empty_width = width.saturating_sub(filled_width);
216
217    // Choose color based on progress
218    let color = if percentage >= 90.0 {
219        "\x1b[32m" // Green for near completion
220    } else if percentage >= 50.0 {
221        "\x1b[33m" // Yellow for halfway
222    } else {
223        "\x1b[31m" // Red for beginning
224    };
225
226    let reset = "\x1b[0m";
227
228    format!(
229        "│{}{}{}{}│",
230        color,
231        "█".repeat(filled_width),
232        reset,
233        " ".repeat(empty_width)
234    )
235}
236
237/// Create an ASCII art progress visualization
238#[allow(dead_code)]
239pub fn ascii_art_progress(percentage: f64) -> String {
240    let blocks = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
241    let width = 20;
242    let progress = percentage / 100.0 * width as f64;
243    let full_blocks = progress.floor() as usize;
244    let partial_block = ((progress - progress.floor()) * (blocks.len() - 1) as f64) as usize;
245
246    let mut result = String::new();
247    result.push_str(&"█".repeat(full_blocks));
248
249    if full_blocks < width && partial_block > 0 {
250        result.push_str(blocks[partial_block]);
251    }
252
253    let remaining = width - full_blocks - if partial_block > 0 { 1 } else { 0 };
254    result.push_str(&" ".repeat(remaining));
255
256    format!("│{result}│")
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_renderer_creation() {
265        let renderer = ProgressRenderer::new();
266        assert_eq!(renderer.last_length, 0);
267        assert_eq!(renderer.spinner_index, 0);
268    }
269
270    #[test]
271    fn test_colored_progress_bar() {
272        let bar_low = colored_progress_bar(25.0, 10);
273        assert!(bar_low.contains("\x1b[31m")); // Red for low progress
274
275        let bar_high = colored_progress_bar(95.0, 10);
276        assert!(bar_high.contains("\x1b[32m")); // Green for high progress
277    }
278
279    #[test]
280    fn test_ascii_art_progress() {
281        let art = ascii_art_progress(50.0);
282        assert!(art.contains("│"));
283        assert!(art.contains("█"));
284    }
285
286    #[test]
287    fn test_console_width() {
288        assert_eq!(console_width("hello"), 5);
289        assert_eq!(console_width("test 123"), 8);
290    }
291}