scirs2_core/logging/progress/
formats.rs1use super::statistics::{format_duration, format_rate, ProgressStats};
7use super::tracker::ProgressSymbols;
8
9pub struct ProgressTemplate {
11 template: String,
13}
14
15impl ProgressTemplate {
16 pub fn new(template: &str) -> Self {
18 Self {
19 template: template.to_string(),
20 }
21 }
22
23 pub fn detailed() -> Self {
25 Self::new("{description}: {bar} {percentage:>6.1}% | {processed}/{total} | {rate} | ETA: {eta} | Elapsed: {elapsed}")
26 }
27
28 pub fn compact() -> Self {
30 Self::new("{description}: {percentage:.1}% ({processed}/{total}), ETA: {eta}")
31 }
32
33 pub fn log_format() -> Self {
35 Self::new("[{timestamp}] {description}: {percentage:.1}% complete ({processed}/{total}) - {rate} - ETA: {eta}")
36 }
37
38 pub fn scientific() -> Self {
40 Self::new("{description}: Progress={percentage:>6.2}% Rate={rate:>10} Remaining={remaining:>8} ETA={eta}")
41 }
42
43 pub fn render(&self, description: &str, stats: &ProgressStats, bar: Option<&str>) -> String {
45 let mut result = self.template.clone();
46
47 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 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 if let Some(bar_str) = bar {
70 result = result.replace("{bar}", bar_str);
71 }
72
73 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 result
82 }
83}
84
85#[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#[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 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 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 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#[derive(Debug, Clone, Default)]
146pub struct ProgressTheme {
147 pub symbols: ProgressSymbols,
149 pub colors: ColorScheme,
151 pub animation: AnimationSettings,
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct ColorScheme {
158 pub fill_color: Option<String>,
160 pub empty_color: Option<String>,
162 pub text_color: Option<String>,
164 pub percentage_color: Option<String>,
166 pub eta_color: Option<String>,
168}
169
170#[derive(Debug, Clone)]
172pub struct AnimationSettings {
173 pub fps: f64,
175 pub animate_spinner: bool,
177 pub animate_bar: bool,
179}
180
181impl ProgressTheme {
182 pub fn modern() -> Self {
184 Self {
185 symbols: ProgressSymbols::blocks(),
186 colors: ColorScheme::colorful(),
187 animation: AnimationSettings::smooth(),
188 }
189 }
190
191 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 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 pub fn colorful() -> Self {
235 Self {
236 fill_color: Some("\x1b[32m".to_string()), empty_color: Some("\x1b[90m".to_string()), text_color: Some("\x1b[37m".to_string()), percentage_color: Some("\x1b[36m".to_string()), eta_color: Some("\x1b[33m".to_string()), }
242 }
243
244 pub fn monochrome() -> Self {
246 Self::default()
247 }
248
249 pub fn scientific() -> Self {
251 Self {
252 fill_color: Some("\x1b[34m".to_string()), empty_color: Some("\x1b[90m".to_string()), text_color: None,
255 percentage_color: Some("\x1b[1m".to_string()), eta_color: Some("\x1b[2m".to_string()), }
258 }
259
260 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 pub fn apply_color(&self, text: &str, color_type: ColorType) -> String {
290 self.format_with_color(text, color_type)
291 }
292}
293
294#[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 pub fn smooth() -> Self {
317 Self {
318 fps: 5.0,
319 animate_spinner: true,
320 animate_bar: true,
321 }
322 }
323
324 pub fn slow() -> Self {
326 Self {
327 fps: 1.0,
328 animate_spinner: true,
329 animate_bar: false,
330 }
331 }
332
333 pub fn precise() -> Self {
335 Self {
336 fps: 1.0,
337 animate_spinner: false,
338 animate_bar: false,
339 }
340 }
341
342 pub fn update_interval(&self) -> std::time::Duration {
344 std::time::Duration::from_secs_f64(1.0 / self.fps)
345 }
346}
347
348pub struct ProgressFormatter;
350
351impl ProgressFormatter {
352 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 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 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 pub fn json(description: &str, stats: &ProgressStats) -> String {
407 Self::format_json(description, stats)
408 }
409
410 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")); assert!(colored.contains("\x1b[0m")); 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}