use console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle};
use std::time::{Duration, Instant};
static CHECK_MARK: Emoji = Emoji("✓", "+");
#[allow(dead_code)]
static CROSS_MARK: Emoji = Emoji("✗", "x");
static SPARKLE: Emoji = Emoji("✨", "*");
static GEAR: Emoji = Emoji("⚙️ ", "");
static PAGE: Emoji = Emoji("📄", "");
pub struct ProgressReporter {
enabled: bool,
spinner: Option<ProgressBar>,
#[allow(dead_code)]
start_time: Instant,
}
impl ProgressReporter {
pub fn new(enabled: bool) -> Self {
Self {
enabled,
spinner: None,
start_time: Instant::now(),
}
}
pub fn start_phase(&mut self, message: &str) {
if !self.enabled {
return;
}
if let Some(ref spinner) = self.spinner {
spinner.finish_and_clear();
}
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.expect("Failed to set progress bar template"),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(Duration::from_millis(100));
self.spinner = Some(pb);
}
pub fn update_message(&self, message: &str) {
if let Some(ref spinner) = self.spinner {
spinner.set_message(message.to_string());
}
}
pub fn finish_phase(&mut self, message: &str, duration: Duration) {
if !self.enabled {
println!("{}", message);
return;
}
if let Some(ref spinner) = self.spinner {
spinner.finish_and_clear();
}
println!(
"{} {} {}",
style(CHECK_MARK).green().bold(),
message,
style(format!("({})", format_duration(duration))).dim()
);
self.spinner = None;
}
#[allow(dead_code)]
pub fn finish_phase_error(&mut self, message: &str) {
if !self.enabled {
eprintln!("Error: {}", message);
return;
}
if let Some(ref spinner) = self.spinner {
spinner.finish_and_clear();
}
eprintln!("{} {}", style(CROSS_MARK).red().bold(), message);
self.spinner = None;
}
#[allow(dead_code)]
pub fn create_bar(&self, total: u64, message: &str) -> Option<ProgressBar> {
if !self.enabled {
return None;
}
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::default_bar()
.template("{msg}\n[{bar:40.cyan/blue}] {percent}% ({pos}/{len} {unit})")
.expect("Failed to set progress bar template")
.progress_chars("=>-"),
);
pb.set_message(message.to_string());
Some(pb)
}
#[allow(dead_code)]
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
}
impl Drop for ProgressReporter {
fn drop(&mut self) {
if let Some(ref spinner) = self.spinner {
spinner.finish_and_clear();
}
}
}
#[derive(Debug, Default)]
pub struct ProcessingStats {
pub nodes_parsed: usize,
pub areas_created: usize,
pub pages_generated: usize,
pub warnings: usize,
pub errors: usize,
pub parse_duration: Duration,
pub layout_duration: Duration,
pub render_duration: Duration,
pub total_duration: Duration,
pub input_size: u64,
pub output_size: u64,
}
impl ProcessingStats {
pub fn new() -> Self {
Self::default()
}
pub fn display(&self) {
println!("\n{} Statistics:", style("Processing").cyan().bold());
println!();
println!(
" {}Parsing: {}",
GEAR,
format_duration(self.parse_duration)
);
println!(
" {}Layout: {}",
GEAR,
format_duration(self.layout_duration)
);
println!(
" {}Rendering: {}",
GEAR,
format_duration(self.render_duration)
);
println!(
" {}Total: {}",
SPARKLE,
format_duration(self.total_duration)
);
println!();
println!(" {}FO Nodes: {}", style("•").dim(), self.nodes_parsed);
println!(" {}Areas: {}", style("•").dim(), self.areas_created);
println!(
" {}Pages: {}",
PAGE,
style(self.pages_generated).cyan().bold()
);
println!();
println!(" Input size: {}", format_bytes(self.input_size));
println!(" Output size: {}", format_bytes(self.output_size));
if self.input_size > 0 {
let ratio = self.output_size as f64 / self.input_size as f64;
println!(" Size ratio: {:.2}x", ratio);
}
println!();
if self.warnings > 0 || self.errors > 0 {
if self.warnings > 0 {
println!(" {}: {}", style("Warnings").yellow().bold(), self.warnings);
}
if self.errors > 0 {
println!(" {}: {}", style("Errors").red().bold(), self.errors);
}
println!();
}
if self.total_duration.as_secs_f64() > 0.0 {
let pages_per_sec = self.pages_generated as f64 / self.total_duration.as_secs_f64();
println!(" Throughput: {:.2} pages/sec", pages_per_sec);
}
}
pub fn display_json(&self) {
let json = format!(
r#"{{
"parsing": {{
"duration_ms": {},
"nodes": {}
}},
"layout": {{
"duration_ms": {},
"areas": {}
}},
"rendering": {{
"duration_ms": {},
"pages": {}
}},
"total": {{
"duration_ms": {},
"input_bytes": {},
"output_bytes": {},
"warnings": {},
"errors": {}
}}
}}"#,
self.parse_duration.as_millis(),
self.nodes_parsed,
self.layout_duration.as_millis(),
self.areas_created,
self.render_duration.as_millis(),
self.pages_generated,
self.total_duration.as_millis(),
self.input_size,
self.output_size,
self.warnings,
self.errors
);
println!("{}", json);
}
}
fn format_duration(duration: Duration) -> String {
let millis = duration.as_millis();
if millis < 1000 {
format!("{}ms", millis)
} else if millis < 60_000 {
format!("{:.2}s", duration.as_secs_f64())
} else {
let mins = millis / 60_000;
let secs = (millis % 60_000) / 1000;
format!("{}m {}s", mins, secs)
}
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes < KB {
format!("{} B", bytes)
} else if bytes < MB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else if bytes < GB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else {
format!("{:.2} GB", bytes as f64 / GB as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_duration() {
assert_eq!(format_duration(Duration::from_millis(100)), "100ms");
assert_eq!(format_duration(Duration::from_millis(1500)), "1.50s");
assert_eq!(format_duration(Duration::from_millis(65000)), "1m 5s");
}
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(100), "100 B");
assert_eq!(format_bytes(1536), "1.50 KB");
assert_eq!(format_bytes(1_572_864), "1.50 MB");
assert_eq!(format_bytes(1_610_612_736), "1.50 GB");
}
#[test]
fn test_stats_creation() {
let stats = ProcessingStats::new();
assert_eq!(stats.nodes_parsed, 0);
assert_eq!(stats.pages_generated, 0);
}
}