scirs2_core/logging/progress/
formats.rs

1//! Progress formatting utilities
2//!
3//! This module provides various formatting options for progress displays,
4//! including templates, themes, and specialized output formats.
5
6use super::statistics::{format_duration, format_rate, ProgressStats};
7use super::tracker::ProgressSymbols;
8
9/// Progress display template
10pub struct ProgressTemplate {
11    /// Template string with placeholders
12    template: String,
13}
14
15impl ProgressTemplate {
16    /// Create a new progress template
17    pub fn new(template: &str) -> Self {
18        Self {
19            template: template.to_string(),
20        }
21    }
22
23    /// Default template for detailed progress
24    pub fn detailed() -> Self {
25        Self::new("{description}: {bar} {percentage:>6.1}% | {processed}/{total} | {rate} | ETA: {eta} | Elapsed: {elapsed}")
26    }
27
28    /// Compact template for minimal display
29    pub fn compact() -> Self {
30        Self::new("{description}: {percentage:.1}% ({processed}/{total}), ETA: {eta}")
31    }
32
33    /// Template suitable for log files
34    pub fn log_format() -> Self {
35        Self::new("[{timestamp}] {description}: {percentage:.1}% complete ({processed}/{total}) - {rate} - ETA: {eta}")
36    }
37
38    /// Template for scientific computation logging
39    pub fn scientific() -> Self {
40        Self::new("{description}: Progress={percentage:>6.2}% Rate={rate:>10} Remaining={remaining:>8} ETA={eta}")
41    }
42
43    /// Render the template with progress statistics
44    pub fn render(&self, description: &str, stats: &ProgressStats, bar: Option<&str>) -> String {
45        let mut result = self.template.clone();
46
47        // Basic replacements
48        result = result.replace("{description}", description);
49        result = result.replace("{percentage}", &format!("{:.1}", stats.percentage));
50        result = result.replace("{processed}", &stats.processed.to_string());
51        result = result.replace("{total}", &stats.total.to_string());
52        result = result.replace("{remaining}", &stats.remaining().to_string());
53        result = result.replace("{rate}", &format_rate(stats.items_per_second));
54        result = result.replace("{eta}", &format_duration(&stats.eta));
55        result = result.replace("{elapsed}", &format_duration(&stats.elapsed));
56
57        // Formatted percentage with custom precision
58        if let Some(captures) = extract_format_spec(&result, "percentage") {
59            let formatted = format!(
60                "{:width$.precision$}",
61                stats.percentage,
62                width = captures.width.unwrap_or(0),
63                precision = captures.precision.unwrap_or(1)
64            );
65            result = result.replace(&captures.original, &formatted);
66        }
67
68        // Progress bar
69        if let Some(bar_str) = bar {
70            result = result.replace("{bar}", bar_str);
71        }
72
73        // Timestamp
74        if result.contains("{timestamp}") {
75            let now = chrono::Utc::now();
76            result = result.replace("{timestamp}", &now.format("%Y-%m-%d %H:%M:%S").to_string());
77        }
78
79        // Additional custom processing could be added here
80
81        result
82    }
83}
84
85/// Format specification extracted from template
86#[derive(Debug)]
87struct FormatSpec {
88    original: String,
89    width: Option<usize>,
90    precision: Option<usize>,
91    #[allow(dead_code)]
92    alignment: Option<char>,
93}
94
95/// Extract format specification from a placeholder
96#[allow(dead_code)]
97fn extract_format_spec(text: &str, field: &str) -> Option<FormatSpec> {
98    let pattern = field.to_string();
99    if let Some(start) = text.find(&pattern) {
100        if let Some(end) = text[start..].find('}') {
101            let spec_str = &text[start..start + end + 1];
102
103            // Parse format specification like {percentage:>6.1}
104            if let Some(colon_pos) = spec_str.find(':') {
105                let format_part = &spec_str[colon_pos + 1..spec_str.len() - 1];
106
107                let mut width = None;
108                let mut precision = None;
109                let mut alignment = None;
110
111                // Parse alignment
112                if format_part.starts_with('<')
113                    || format_part.starts_with('>')
114                    || format_part.starts_with('^')
115                {
116                    alignment = format_part.chars().next();
117                }
118
119                // Parse width and precision
120                let numeric_part = format_part.trim_start_matches(['<', '>', '^']);
121                if let Some(dot_pos) = numeric_part.find('.') {
122                    if let Ok(w) = numeric_part[..dot_pos].parse::<usize>() {
123                        width = Some(w);
124                    }
125                    if let Ok(p) = numeric_part[dot_pos + 1..].parse::<usize>() {
126                        precision = Some(p);
127                    }
128                } else if let Ok(w) = numeric_part.parse::<usize>() {
129                    width = Some(w);
130                }
131
132                return Some(FormatSpec {
133                    original: spec_str.to_string(),
134                    width,
135                    precision,
136                    alignment,
137                });
138            }
139        }
140    }
141    None
142}
143
144/// Progress display theme
145#[derive(Debug, Clone, Default)]
146pub struct ProgressTheme {
147    /// Symbols for progress visualization
148    pub symbols: ProgressSymbols,
149    /// Color scheme
150    pub colors: ColorScheme,
151    /// Animation settings
152    pub animation: AnimationSettings,
153}
154
155/// Color scheme for progress display
156#[derive(Debug, Clone, Default)]
157pub struct ColorScheme {
158    /// Color for progress bar fill
159    pub fill_color: Option<String>,
160    /// Color for progress bar empty
161    pub empty_color: Option<String>,
162    /// Color for text
163    pub text_color: Option<String>,
164    /// Color for percentage
165    pub percentage_color: Option<String>,
166    /// Color for ETA
167    pub eta_color: Option<String>,
168}
169
170/// Animation settings
171#[derive(Debug, Clone)]
172pub struct AnimationSettings {
173    /// Animation speed (frames per second)
174    pub fps: f64,
175    /// Whether to animate spinner
176    pub animate_spinner: bool,
177    /// Whether to animate progress bar
178    pub animate_bar: bool,
179}
180
181impl ProgressTheme {
182    /// Modern theme with Unicode blocks
183    pub fn modern() -> Self {
184        Self {
185            symbols: ProgressSymbols::blocks(),
186            colors: ColorScheme::colorful(),
187            animation: AnimationSettings::smooth(),
188        }
189    }
190
191    /// Minimal theme for simple terminals
192    pub fn minimal() -> Self {
193        Self {
194            symbols: ProgressSymbols {
195                start: "[".to_string(),
196                end: "]".to_string(),
197                fill: "#".to_string(),
198                empty: "-".to_string(),
199                spinner: vec![
200                    "|".to_string(),
201                    "/".to_string(),
202                    "-".to_string(),
203                    "\\".to_string(),
204                ],
205            },
206            colors: ColorScheme::monochrome(),
207            animation: AnimationSettings::slow(),
208        }
209    }
210
211    /// Scientific theme with precise formatting
212    pub fn scientific() -> Self {
213        Self {
214            symbols: ProgressSymbols {
215                start: "│".to_string(),
216                end: "│".to_string(),
217                fill: "█".to_string(),
218                empty: "░".to_string(),
219                spinner: vec![
220                    "◐".to_string(),
221                    "◓".to_string(),
222                    "◑".to_string(),
223                    "◒".to_string(),
224                ],
225            },
226            colors: ColorScheme::scientific(),
227            animation: AnimationSettings::precise(),
228        }
229    }
230}
231
232impl ColorScheme {
233    /// Colorful scheme with ANSI colors
234    pub fn colorful() -> Self {
235        Self {
236            fill_color: Some("\x1b[32m".to_string()),       // Green
237            empty_color: Some("\x1b[90m".to_string()),      // Dark gray
238            text_color: Some("\x1b[37m".to_string()),       // White
239            percentage_color: Some("\x1b[36m".to_string()), // Cyan
240            eta_color: Some("\x1b[33m".to_string()),        // Yellow
241        }
242    }
243
244    /// Monochrome scheme
245    pub fn monochrome() -> Self {
246        Self::default()
247    }
248
249    /// Scientific color scheme with subtle colors
250    pub fn scientific() -> Self {
251        Self {
252            fill_color: Some("\x1b[34m".to_string()),  // Blue
253            empty_color: Some("\x1b[90m".to_string()), // Dark gray
254            text_color: None,
255            percentage_color: Some("\x1b[1m".to_string()), // Bold
256            eta_color: Some("\x1b[2m".to_string()),        // Dim
257        }
258    }
259
260    /// Apply color to text
261    pub fn format_with_color(&self, text: &str, colortype: ColorType) -> String {
262        let color = match colortype {
263            ColorType::Fill => &self.fill_color,
264            ColorType::Empty => &self.empty_color,
265            ColorType::Text => &self.text_color,
266            ColorType::Percentage => &self.percentage_color,
267            ColorType::ETA => &self.eta_color,
268        };
269
270        if let Some(colorcode) = color {
271            format!("{colorcode}{text}\x1b[0m")
272        } else {
273            text.to_string()
274        }
275    }
276
277    /// Apply color to text (alias for format_with_color)
278    ///
279    /// This method applies ANSI color codes to text based on the specified color type.
280    ///
281    /// # Arguments
282    ///
283    /// * `text` - The text to colorize
284    /// * `color_type` - The type of color to apply
285    ///
286    /// # Returns
287    ///
288    /// Colored text with ANSI escape sequences
289    pub fn apply_color(&self, text: &str, color_type: ColorType) -> String {
290        self.format_with_color(text, color_type)
291    }
292}
293
294/// Color type for applying colors
295#[derive(Debug, Clone, Copy)]
296pub enum ColorType {
297    Fill,
298    Empty,
299    Text,
300    Percentage,
301    ETA,
302}
303
304impl Default for AnimationSettings {
305    fn default() -> Self {
306        Self {
307            fps: 2.0,
308            animate_spinner: true,
309            animate_bar: false,
310        }
311    }
312}
313
314impl AnimationSettings {
315    /// Smooth animation settings
316    pub fn smooth() -> Self {
317        Self {
318            fps: 5.0,
319            animate_spinner: true,
320            animate_bar: true,
321        }
322    }
323
324    /// Slow animation settings
325    pub fn slow() -> Self {
326        Self {
327            fps: 1.0,
328            animate_spinner: true,
329            animate_bar: false,
330        }
331    }
332
333    /// Precise animation for scientific use
334    pub fn precise() -> Self {
335        Self {
336            fps: 1.0,
337            animate_spinner: false,
338            animate_bar: false,
339        }
340    }
341
342    /// Get update interval based on FPS
343    pub fn update_interval(&self) -> std::time::Duration {
344        std::time::Duration::from_secs_f64(1.0 / self.fps)
345    }
346}
347
348/// Specialized formatter for different output formats
349pub struct ProgressFormatter;
350
351impl ProgressFormatter {
352    /// Format for JSON output
353    pub fn format_json(description: &str, stats: &ProgressStats) -> String {
354        serde_json::json!({
355            "_description": description,
356            "processed": stats.processed,
357            "total": stats.total,
358            "percentage": stats.percentage,
359            "rate": stats.items_per_second,
360            "eta_seconds": stats.eta.as_secs(),
361            "elapsed_seconds": stats.elapsed.as_secs()
362        })
363        .to_string()
364    }
365
366    /// Format for CSV output
367    pub fn format_csv(description: &str, stats: &ProgressStats) -> String {
368        format!(
369            "{},{},{},{:.2},{:.2},{},{}",
370            description,
371            stats.processed,
372            stats.total,
373            stats.percentage,
374            stats.items_per_second,
375            stats.eta.as_secs(),
376            stats.elapsed.as_secs()
377        )
378    }
379
380    /// Format for machine-readable output
381    pub fn format_machine(description: &str, stats: &ProgressStats) -> String {
382        format!(
383            "PROGRESS|{}|{}|{}|{:.2}|{:.2}|{}|{}",
384            description,
385            stats.processed,
386            stats.total,
387            stats.percentage,
388            stats.items_per_second,
389            stats.eta.as_secs(),
390            stats.elapsed.as_secs()
391        )
392    }
393
394    /// JSON output (alias for format_json)
395    ///
396    /// Returns progress information formatted as JSON string.
397    ///
398    /// # Arguments
399    ///
400    /// * `description` - Description of the progress task
401    /// * `stats` - Progress statistics
402    ///
403    /// # Returns
404    ///
405    /// JSON formatted string containing progress data
406    pub fn json(description: &str, stats: &ProgressStats) -> String {
407        Self::format_json(description, stats)
408    }
409
410    /// CSV output (alias for format_csv)
411    ///
412    /// Returns progress information formatted as CSV string.
413    ///
414    /// # Arguments
415    ///
416    /// * `description` - Description of the progress task  
417    /// * `stats` - Progress statistics
418    ///
419    /// # Returns
420    ///
421    /// CSV formatted string containing progress data
422    pub fn csv(description: &str, stats: &ProgressStats) -> String {
423        Self::format_csv(description, stats)
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_progress_template_render() {
433        let template =
434            ProgressTemplate::new("{description}: {percentage:.1}% ({processed}/{total})");
435        let stats = ProgressStats::new(100);
436
437        let result = template.render("Test", &stats, None);
438        assert!(result.contains("Test"));
439        assert!(result.contains("0.0%"));
440        assert!(result.contains("0/100"));
441    }
442
443    #[test]
444    fn test_format_spec_extraction() {
445        let spec = extract_format_spec("{percentage:>6.1}", "percentage");
446        assert!(spec.is_some());
447        let spec = spec.expect("Operation failed");
448        assert_eq!(spec.width, Some(6));
449        assert_eq!(spec.precision, Some(1));
450    }
451
452    #[test]
453    fn test_color_scheme_apply() {
454        let colors = ColorScheme::colorful();
455        let colored = colors.apply_color("test", ColorType::Fill);
456        assert!(colored.contains("\x1b[32m")); // Green
457        assert!(colored.contains("\x1b[0m")); // Reset
458        assert!(colors.fill_color.is_some());
459    }
460
461    #[test]
462    fn test_progress_formatter_json() {
463        let stats = ProgressStats::new(100);
464        let json_output = ProgressFormatter::json("Test", &stats);
465        assert!(json_output.contains("\"_description\":\"Test\""));
466        assert!(json_output.contains("\"total\":100"));
467        assert_eq!(stats.total, 100);
468    }
469
470    #[test]
471    fn test_progress_formatter_csv() {
472        let stats = ProgressStats::new(100);
473        let csv_output = ProgressFormatter::csv("Test", &stats);
474        assert!(csv_output.starts_with("Test,"));
475        assert!(csv_output.contains(",100,"));
476        assert_eq!(stats.total, 100);
477    }
478}