use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::time::{Duration, Instant};
static GLOBAL_UNIFIED_PROGRESS: Lazy<Mutex<Option<AnalysisProgress>>> =
Lazy::new(|| Mutex::new(None));
pub struct AnalysisProgress {
phases: Vec<AnalysisPhase>,
current_phase: usize,
start_time: Instant,
interactive: bool,
last_update: Instant,
}
#[derive(Debug)]
struct AnalysisPhase {
name: &'static str,
status: PhaseStatus,
start_time: Option<Instant>,
duration: Option<Duration>,
progress: PhaseProgress,
printed_start: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum PhaseStatus {
Pending,
InProgress,
Complete,
}
#[derive(Debug, Clone)]
pub enum PhaseProgress {
Indeterminate,
Count(usize),
Progress { current: usize, total: usize },
}
impl AnalysisProgress {
pub fn new() -> Self {
use std::io::IsTerminal;
let is_interactive = std::io::stderr().is_terminal();
Self {
phases: vec![
AnalysisPhase::new("files parse", PhaseProgress::Indeterminate),
AnalysisPhase::new("Building call graph", PhaseProgress::Indeterminate),
],
current_phase: 0,
start_time: Instant::now(),
interactive: is_interactive,
last_update: Instant::now(),
}
}
pub fn init_global() {
*GLOBAL_UNIFIED_PROGRESS.lock() = Some(Self::new());
}
pub fn with_global<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut AnalysisProgress) -> R,
{
let mut guard = GLOBAL_UNIFIED_PROGRESS.lock();
guard.as_mut().map(f)
}
pub fn clear_global() {
*GLOBAL_UNIFIED_PROGRESS.lock() = None;
}
pub fn start_phase(&mut self, phase_index: usize) {
if phase_index >= self.phases.len() {
return;
}
self.current_phase = phase_index;
self.phases[phase_index].status = PhaseStatus::InProgress;
self.phases[phase_index].start_time = Some(Instant::now());
self.phases[phase_index].printed_start = false; self.render();
}
pub fn update_progress(&mut self, progress: PhaseProgress) {
if self.current_phase >= self.phases.len() {
return;
}
self.phases[self.current_phase].progress = progress;
if self.should_update() {
self.render();
self.last_update = Instant::now();
}
}
pub fn complete_phase(&mut self) {
if self.current_phase >= self.phases.len() {
return;
}
let phase = &mut self.phases[self.current_phase];
phase.status = PhaseStatus::Complete;
if let Some(start) = phase.start_time {
phase.duration = Some(start.elapsed());
}
self.render();
}
pub fn finish(&self) {
let total_duration = self.start_time.elapsed();
eprintln!(
"\nAnalysis complete in {:.1}s",
total_duration.as_secs_f64()
);
}
fn should_update(&self) -> bool {
self.last_update.elapsed() > Duration::from_millis(100)
}
fn render(&mut self) {
if self.current_phase >= self.phases.len() {
return;
}
if crate::progress::ProgressManager::global()
.and_then(|m| m.is_tui_active())
.unwrap_or(false)
{
return;
}
let phase_index = self.current_phase;
let phase = &self.phases[phase_index];
let phase_num = self.current_phase + 1;
let total_phases = self.phases.len();
if self.interactive {
let indicator = match phase.status {
PhaseStatus::InProgress => "→",
PhaseStatus::Complete => "✓",
PhaseStatus::Pending => " ",
};
let progress_str = format_progress(&phase.progress);
let duration_str = phase
.duration
.map(|d| format!(" - {}s", d.as_secs()))
.unwrap_or_default();
eprint!(
"\r\x1b[K{} {}/{} {}...{}{}",
indicator, phase_num, total_phases, phase.name, progress_str, duration_str
);
if matches!(phase.status, PhaseStatus::Complete) {
eprintln!();
}
} else {
match phase.status {
PhaseStatus::Complete => {
let duration = phase
.duration
.map(|d| format!("{}s", d.as_secs()))
.unwrap_or_else(|| "0s".to_string());
eprintln!(
"✓ {}/{} {} - {}",
phase_num, total_phases, phase.name, duration
);
}
PhaseStatus::InProgress => {
if !self.phases[phase_index].printed_start {
eprintln!("→ {}/{} {}...", phase_num, total_phases, phase.name);
self.phases[phase_index].printed_start = true;
}
}
_ => {}
}
}
}
}
impl Default for AnalysisProgress {
fn default() -> Self {
Self::new()
}
}
impl AnalysisPhase {
fn new(name: &'static str, progress: PhaseProgress) -> Self {
Self {
name,
status: PhaseStatus::Pending,
start_time: None,
duration: None,
progress,
printed_start: false,
}
}
}
fn format_progress(progress: &PhaseProgress) -> String {
match progress {
PhaseProgress::Indeterminate => String::new(),
PhaseProgress::Count(n) => format!("{} found", n),
PhaseProgress::Progress { current, total } => {
if *total == 0 {
"0/0 (0%)".to_string()
} else {
let pct = (*current as f64 / *total as f64 * 100.0) as usize;
format!("{}/{} ({}%)", current, total, pct)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_lifecycle() {
let mut progress = AnalysisProgress::new();
progress.start_phase(0);
assert_eq!(progress.current_phase, 0);
assert!(matches!(progress.phases[0].status, PhaseStatus::InProgress));
progress.update_progress(PhaseProgress::Count(100));
assert!(matches!(
progress.phases[0].progress,
PhaseProgress::Count(100)
));
progress.complete_phase();
assert!(matches!(progress.phases[0].status, PhaseStatus::Complete));
assert!(progress.phases[0].duration.is_some());
}
#[test]
fn test_format_phase_progress_count() {
let progress = PhaseProgress::Count(511);
let formatted = format_progress(&progress);
assert_eq!(formatted, "511 found");
}
#[test]
fn test_format_phase_progress_fractional() {
let progress = PhaseProgress::Progress {
current: 256,
total: 511,
};
let formatted = format_progress(&progress);
assert_eq!(formatted, "256/511 (50%)");
}
#[test]
fn test_format_phase_progress_complete() {
let progress = PhaseProgress::Progress {
current: 511,
total: 511,
};
let formatted = format_progress(&progress);
assert_eq!(formatted, "511/511 (100%)");
}
#[test]
fn test_format_phase_progress_zero_total() {
let progress = PhaseProgress::Progress {
current: 0,
total: 0,
};
let formatted = format_progress(&progress);
assert_eq!(formatted, "0/0 (0%)");
}
#[test]
fn test_indeterminate_progress() {
let progress = PhaseProgress::Indeterminate;
let formatted = format_progress(&progress);
assert_eq!(formatted, "");
}
#[test]
fn test_multiple_phases() {
let mut progress = AnalysisProgress::new();
progress.start_phase(0);
progress.update_progress(PhaseProgress::Count(511));
progress.complete_phase();
assert!(matches!(progress.phases[0].status, PhaseStatus::Complete));
progress.start_phase(1);
progress.update_progress(PhaseProgress::Progress {
current: 511,
total: 511,
});
progress.complete_phase();
assert!(matches!(progress.phases[1].status, PhaseStatus::Complete));
assert!(progress.phases[0].duration.is_some());
assert!(progress.phases[1].duration.is_some());
}
}