use crate::common;
use crate::terminal;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use owo_colors::OwoColorize;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
pub struct Tracker {
bar: ProgressBar,
done: Arc<AtomicBool>,
}
impl Tracker {
#[must_use]
pub fn new(label: &str) -> Self {
Self::with_target(label, ProgressDrawTarget::stderr_with_hz(10))
}
#[must_use]
pub fn with_target(label: &str, target: ProgressDrawTarget) -> Self {
let done = Arc::new(AtomicBool::new(false));
let bar = ProgressBar::with_draw_target(Some(100), target);
let style = ProgressStyle::with_template(
" {prefix} {bar:40.cyan/blue} {percent:>3}% {elapsed_precise} | {msg}",
)
.unwrap()
.progress_chars("█░─");
bar.set_style(style);
bar.set_prefix(if terminal::no_color() {
format!("{:<10}", format!("{}:", label))
} else {
format!("{:<10}", format!("{label}:").dimmed())
});
bar.set_message("starting...");
bar.set_position(0);
Self { bar, done }
}
pub fn update(&self, speed_mbps: f64, progress: f64, bytes: u64) {
let speed_str = if speed_mbps < 1000.0 {
format!("{speed_mbps:.1} Mb/s")
} else {
format!("{:.2} Gb/s", speed_mbps / 1000.0)
};
let data_str = common::format_data_size(bytes);
let msg = if terminal::no_color() {
format!("{data_str} @ {speed_str}")
} else {
format!("{} @ {}", data_str.white(), speed_str.cyan())
};
self.bar.set_message(msg);
let pct = (progress * 100.0).clamp(0.0, u64::MAX as f64) as u64;
self.bar.set_position(pct.min(100));
}
pub fn finish(&self, final_speed_mbps: f64, total_bytes: u64) {
let speed_str = if final_speed_mbps < 1000.0 {
format!("{final_speed_mbps:.2} Mb/s")
} else {
format!("{:.2} Gb/s", final_speed_mbps / 1000.0)
};
let data_str = common::format_data_size(total_bytes);
self.bar.set_position(100);
let msg = if terminal::no_color() {
format!("DONE ({data_str} total @ {speed_str})")
} else {
format!(
"{} ({} total @ {})",
"DONE".green().bold(),
data_str.dimmed(),
speed_str.green()
)
};
self.bar.finish_with_message(msg);
self.done.store(true, Ordering::Relaxed);
}
}
#[must_use]
pub fn create_spinner(message: &str) -> ProgressBar {
let pb = ProgressBar::with_draw_target(None, ProgressDrawTarget::stderr_with_hz(10));
pb.set_style(
ProgressStyle::with_template(" {spinner} {msg}")
.unwrap()
.tick_strings(&["·", "o", "O", "o"]),
);
pb.set_message(message.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(120));
pb
}
pub fn finish_ok(pb: &ProgressBar, message: &str) {
if terminal::no_color() {
pb.finish_with_message(format!(" {message}"));
} else {
pb.finish_with_message(format!(" {} {}", "✓".green(), message));
}
}
pub fn reveal_grade(label: &str, grade_str: &str, grade_plain: &str, nc: bool) {
if nc {
std::thread::sleep(Duration::from_millis(300));
eprintln!(" {} → {grade_plain}", label.dimmed());
} else {
let spinner = create_spinner(&format!("Computing {label}..."));
std::thread::sleep(Duration::from_millis(400));
spinner.finish_and_clear();
eprintln!(" {label} → {grade_str}");
}
}
pub fn reveal_scan_complete(sample_count: usize, grade_badge: &str, grade_plain: &str, nc: bool) {
if terminal::no_animation() {
eprintln!(" SCAN COMPLETE ✓ Scanned {sample_count} samples → {grade_plain}");
} else if nc {
std::thread::sleep(Duration::from_millis(100));
eprintln!(
" {} ✓ Scanned {sample_count} samples → Grade: {grade_plain}",
"SCAN COMPLETE".bold()
);
} else {
std::thread::sleep(Duration::from_millis(100));
eprintln!(
" {} {} Scanned {} samples → {}",
"SCAN COMPLETE".cyan().bold(),
"✓".green(),
sample_count.to_string().white().bold(),
grade_badge,
);
}
}
pub fn reveal_pause() {
if terminal::no_animation() {
return;
}
std::thread::sleep(Duration::from_millis(40));
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
fn set_no_color() {
#[allow(unsafe_code)]
unsafe {
std::env::set_var("NO_COLOR", "1");
}
}
fn unset_no_color() {
#[allow(unsafe_code)]
unsafe {
std::env::remove_var("NO_COLOR");
}
}
#[test]
fn test_no_color_default() {
let _ = terminal::no_color();
}
#[test]
fn test_create_spinner() {
let pb = create_spinner("Testing...");
assert!(!pb.is_finished());
pb.finish_and_clear();
}
#[test]
fn test_finish_ok() {
let pb = create_spinner("Testing...");
finish_ok(&pb, "Done");
assert!(pb.is_finished());
}
#[test]
fn test_speed_progress_new() {
let sp = Tracker::new("Download");
assert!(!sp.done.load(Ordering::Relaxed));
sp.bar.finish_and_clear();
}
#[test]
fn test_speed_progress_update() {
let sp = Tracker::new("Download");
sp.update(150.5, 0.5, 1024 * 1024);
assert_eq!(sp.bar.position(), 50);
sp.bar.finish_and_clear();
}
#[test]
fn test_speed_progress_nc() {
set_no_color();
let sp = Tracker::new("Upload");
sp.update(50.0, 0.25, 512 * 1024);
assert_eq!(sp.bar.position(), 25);
sp.finish(50.0, 1024 * 1024);
assert!(sp.done.load(Ordering::Relaxed));
unset_no_color();
}
#[test]
#[serial]
fn test_no_color_env_set() {
set_no_color();
assert!(terminal::no_color());
unset_no_color();
}
#[test]
#[serial]
fn test_create_spinner_nc() {
set_no_color();
let pb = create_spinner("Testing...");
assert!(!pb.is_finished());
pb.finish_and_clear();
unset_no_color();
}
#[test]
#[serial]
fn test_finish_ok_nc() {
set_no_color();
let pb = create_spinner("Testing...");
finish_ok(&pb, "Done");
assert!(pb.is_finished());
unset_no_color();
}
#[test]
#[serial]
fn test_reveal_grade_nc() {
set_no_color();
reveal_grade("Overall", "A", "A", true);
unset_no_color();
}
#[test]
#[serial]
fn test_reveal_scan_complete_nc() {
set_no_color();
reveal_scan_complete(42, "B+", "B+", true);
unset_no_color();
}
#[test]
fn test_reveal_pause() {
reveal_pause();
}
}