1use crate::common;
15use crate::terminal;
16use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
17use owo_colors::OwoColorize;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::time::Duration;
21
22pub struct Tracker {
25 bar: ProgressBar,
26 done: Arc<AtomicBool>,
27}
28
29impl Tracker {
30 #[must_use]
33 pub fn new(label: &str) -> Self {
34 Self::with_target(label, ProgressDrawTarget::stderr_with_hz(10))
35 }
36
37 #[must_use]
43 pub fn with_target(label: &str, target: ProgressDrawTarget) -> Self {
44 let done = Arc::new(AtomicBool::new(false));
45 let bar = ProgressBar::with_draw_target(Some(100), target);
46
47 let style = ProgressStyle::with_template(
48 " {prefix} {bar:40.cyan/blue} {percent:>3}% {elapsed_precise} | {msg}",
49 )
50 .unwrap()
51 .progress_chars("█░─");
52
53 bar.set_style(style);
54 bar.set_prefix(if terminal::no_color() {
55 format!("{:<10}", format!("{}:", label))
56 } else {
57 format!("{:<10}", format!("{label}:").dimmed())
58 });
59 bar.set_message("starting...");
60 bar.set_position(0);
61
62 Self { bar, done }
63 }
64
65 pub fn update(&self, speed_mbps: f64, progress: f64, bytes: u64) {
70 let speed_str = if speed_mbps < 1000.0 {
71 format!("{speed_mbps:.1} Mb/s")
72 } else {
73 format!("{:.2} Gb/s", speed_mbps / 1000.0)
74 };
75
76 let data_str = common::format_data_size(bytes);
77
78 let msg = if terminal::no_color() {
79 format!("{data_str} @ {speed_str}")
80 } else {
81 format!("{} @ {}", data_str.white(), speed_str.cyan())
82 };
83
84 self.bar.set_message(msg);
85 let pct = (progress * 100.0).clamp(0.0, u64::MAX as f64) as u64;
87 self.bar.set_position(pct.min(100));
88 }
89
90 pub fn finish(&self, final_speed_mbps: f64, total_bytes: u64) {
92 let speed_str = if final_speed_mbps < 1000.0 {
93 format!("{final_speed_mbps:.2} Mb/s")
94 } else {
95 format!("{:.2} Gb/s", final_speed_mbps / 1000.0)
96 };
97
98 let data_str = common::format_data_size(total_bytes);
99
100 self.bar.set_position(100);
101 let msg = if terminal::no_color() {
102 format!("DONE ({data_str} total @ {speed_str})")
103 } else {
104 format!(
105 "{} ({} total @ {})",
106 "DONE".green().bold(),
107 data_str.dimmed(),
108 speed_str.green()
109 )
110 };
111 self.bar.finish_with_message(msg);
112 self.done.store(true, Ordering::Relaxed);
113 }
114}
115
116#[must_use]
122pub fn create_spinner(message: &str) -> ProgressBar {
123 let pb = ProgressBar::with_draw_target(None, ProgressDrawTarget::stderr_with_hz(10));
124 pb.set_style(
125 ProgressStyle::with_template(" {spinner} {msg}")
126 .unwrap()
127 .tick_strings(&["·", "o", "O", "o"]),
128 );
129 pb.set_message(message.to_string());
130 pb.enable_steady_tick(std::time::Duration::from_millis(120));
131 pb
132}
133
134pub fn finish_ok(pb: &ProgressBar, message: &str) {
136 if terminal::no_color() {
137 pb.finish_with_message(format!(" {message}"));
138 } else {
139 pb.finish_with_message(format!(" {} {}", "✓".green(), message));
140 }
141}
142
143pub fn reveal_grade(label: &str, grade_str: &str, grade_plain: &str, nc: bool) {
154 if nc {
155 std::thread::sleep(Duration::from_millis(300));
157 eprintln!(" {} → {grade_plain}", label.dimmed());
158 } else {
159 let spinner = create_spinner(&format!("Computing {label}..."));
161 std::thread::sleep(Duration::from_millis(400));
162 spinner.finish_and_clear();
163 eprintln!(" {label} → {grade_str}");
164 }
165}
166
167pub fn reveal_scan_complete(sample_count: usize, grade_badge: &str, grade_plain: &str, nc: bool) {
176 if terminal::no_animation() {
177 eprintln!(" SCAN COMPLETE ✓ Scanned {sample_count} samples → {grade_plain}");
179 } else if nc {
180 std::thread::sleep(Duration::from_millis(100));
181 eprintln!(
182 " {} ✓ Scanned {sample_count} samples → Grade: {grade_plain}",
183 "SCAN COMPLETE".bold()
184 );
185 } else {
186 std::thread::sleep(Duration::from_millis(100));
188 eprintln!(
189 " {} {} Scanned {} samples → {}",
190 "SCAN COMPLETE".cyan().bold(),
191 "✓".green(),
192 sample_count.to_string().white().bold(),
193 grade_badge,
194 );
195 }
196}
197
198pub fn reveal_pause() {
200 if terminal::no_animation() {
201 return;
202 }
203 std::thread::sleep(Duration::from_millis(40));
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use serial_test::serial;
210
211 fn set_no_color() {
217 #[allow(unsafe_code)]
218 unsafe {
219 std::env::set_var("NO_COLOR", "1");
220 }
221 }
222
223 fn unset_no_color() {
228 #[allow(unsafe_code)]
229 unsafe {
230 std::env::remove_var("NO_COLOR");
231 }
232 }
233
234 #[test]
235 fn test_no_color_default() {
236 let _ = terminal::no_color();
239 }
240
241 #[test]
242 fn test_create_spinner() {
243 let pb = create_spinner("Testing...");
244 assert!(!pb.is_finished());
245 pb.finish_and_clear();
246 }
247
248 #[test]
249 fn test_finish_ok() {
250 let pb = create_spinner("Testing...");
251 finish_ok(&pb, "Done");
252 assert!(pb.is_finished());
253 }
254
255 #[test]
256 fn test_speed_progress_new() {
257 let sp = Tracker::new("Download");
258 assert!(!sp.done.load(Ordering::Relaxed));
259 sp.bar.finish_and_clear();
260 }
261
262 #[test]
263 fn test_speed_progress_update() {
264 let sp = Tracker::new("Download");
265 sp.update(150.5, 0.5, 1024 * 1024);
266 assert_eq!(sp.bar.position(), 50);
267 sp.bar.finish_and_clear();
268 }
269
270 #[test]
271 fn test_speed_progress_nc() {
272 set_no_color();
273 let sp = Tracker::new("Upload");
274 sp.update(50.0, 0.25, 512 * 1024);
275 assert_eq!(sp.bar.position(), 25);
276 sp.finish(50.0, 1024 * 1024);
277 assert!(sp.done.load(Ordering::Relaxed));
278 unset_no_color();
279 }
280
281 #[test]
282 #[serial]
283 fn test_no_color_env_set() {
284 set_no_color();
285 assert!(terminal::no_color());
286 unset_no_color();
287 }
288
289 #[test]
290 #[serial]
291 fn test_create_spinner_nc() {
292 set_no_color();
293 let pb = create_spinner("Testing...");
294 assert!(!pb.is_finished());
295 pb.finish_and_clear();
296 unset_no_color();
297 }
298
299 #[test]
300 #[serial]
301 fn test_finish_ok_nc() {
302 set_no_color();
303 let pb = create_spinner("Testing...");
304 finish_ok(&pb, "Done");
305 assert!(pb.is_finished());
306 unset_no_color();
307 }
308
309 #[test]
310 #[serial]
311 fn test_reveal_grade_nc() {
312 set_no_color();
313 reveal_grade("Overall", "A", "A", true);
314 unset_no_color();
315 }
316
317 #[test]
318 #[serial]
319 fn test_reveal_scan_complete_nc() {
320 set_no_color();
321 reveal_scan_complete(42, "B+", "B+", true);
322 unset_no_color();
323 }
324
325 #[test]
326 fn test_reveal_pause() {
327 reveal_pause();
329 }
330}