1#![allow(
10 clippy::cast_precision_loss,
11 clippy::cast_possible_truncation,
12 clippy::cast_sign_loss
13)]
14
15use crate::common;
16use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
17use owo_colors::OwoColorize;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicBool, Ordering};
20
21#[must_use]
23pub fn no_color() -> bool {
24 std::env::var("NO_COLOR").is_ok()
25}
26
27pub struct SpeedProgress {
30 bar: ProgressBar,
31 done: Arc<AtomicBool>,
32}
33
34impl SpeedProgress {
35 #[must_use]
38 pub fn new(label: &str) -> Self {
39 Self::with_target(label, ProgressDrawTarget::stderr_with_hz(10))
40 }
41
42 #[must_use]
44 pub fn with_target(label: &str, target: ProgressDrawTarget) -> Self {
45 let done = Arc::new(AtomicBool::new(false));
46 let bar = ProgressBar::with_draw_target(Some(100), target);
47
48 let style = ProgressStyle::with_template(
49 " {prefix} {bar:40.cyan/blue} {percent:>3}% {elapsed_precise} | {msg}",
50 )
51 .unwrap()
52 .progress_chars("━╾─");
53
54 bar.set_style(style);
55 bar.set_prefix(if no_color() {
56 format!("{:<10}", format!("{}:", label))
57 } else {
58 format!("{:<10}", format!("{label}:").dimmed())
59 });
60 bar.set_message("starting...");
61 bar.set_position(0);
62
63 Self { bar, done }
64 }
65
66 pub fn update(&self, speed_mbps: f64, progress: f64, bytes: u64) {
71 let speed_str = if speed_mbps < 1000.0 {
72 format!("{speed_mbps:.1} Mb/s")
73 } else {
74 format!("{:.2} Gb/s", speed_mbps / 1000.0)
75 };
76
77 let data_str = common::format_data_size(bytes);
78
79 let msg = if no_color() {
80 format!("{data_str} @ {speed_str}")
81 } else {
82 format!("{} @ {}", data_str.white(), speed_str.cyan())
83 };
84
85 self.bar.set_message(msg);
86 let pct = (progress * 100.0) 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 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]
118pub fn create_spinner(message: &str) -> ProgressBar {
119 let pb = ProgressBar::with_draw_target(None, ProgressDrawTarget::stderr_with_hz(10));
120 pb.set_style(
121 ProgressStyle::with_template(" {spinner} {msg}")
122 .unwrap()
123 .tick_strings(&["·", "o", "O", "o"]),
124 );
125 pb.set_message(message.to_string());
126 pb.enable_steady_tick(std::time::Duration::from_millis(120));
127 pb
128}
129
130pub fn finish_ok(pb: &ProgressBar, message: &str) {
132 if no_color() {
133 pb.finish_with_message(format!(" {message}"));
134 } else {
135 pb.finish_with_message(format!(" {} {}", "✓".green(), message));
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use serial_test::serial;
143
144 fn set_no_color() {
146 unsafe { std::env::set_var("NO_COLOR", "1") }
148 }
149
150 fn unset_no_color() {
152 unsafe { std::env::remove_var("NO_COLOR") }
154 }
155
156 #[test]
157 fn test_no_color_default() {
158 let _ = no_color();
161 }
162
163 #[test]
164 fn test_create_spinner() {
165 let pb = create_spinner("Testing...");
166 assert!(!pb.is_finished());
167 pb.finish_and_clear();
168 }
169
170 #[test]
171 fn test_finish_ok() {
172 let pb = create_spinner("Testing...");
173 finish_ok(&pb, "Done");
174 assert!(pb.is_finished());
175 }
176
177 #[test]
178 fn test_speed_progress_new() {
179 let sp = SpeedProgress::new("Download");
180 assert!(!sp.done.load(Ordering::Relaxed));
181 sp.bar.finish_and_clear();
182 }
183
184 #[test]
185 fn test_speed_progress_update() {
186 let sp = SpeedProgress::new("Download");
187 sp.update(125.4, 0.5, 5_000_000);
188 sp.finish(125.40, 10_000_000);
189 assert!(sp.done.load(Ordering::Relaxed));
190 }
191
192 #[test]
193 #[serial]
194 fn test_no_color_env_set() {
195 set_no_color();
196 assert!(no_color());
197 unset_no_color();
198 }
199
200 #[test]
201 #[serial]
202 fn test_create_spinner_nc() {
203 set_no_color();
204 let pb = create_spinner("Testing...");
205 assert!(!pb.is_finished());
206 pb.finish_and_clear();
207 unset_no_color();
208 }
209
210 #[test]
211 #[serial]
212 fn test_finish_ok_nc() {
213 set_no_color();
214 let pb = create_spinner("Testing...");
215 finish_ok(&pb, "Done");
216 assert!(pb.is_finished());
217 unset_no_color();
218 }
219
220 #[test]
221 #[serial]
222 fn test_speed_progress_nc() {
223 set_no_color();
224 let sp = SpeedProgress::new("Download");
225 sp.update(125.4, 0.5, 5_000_000);
226 sp.finish(125.40, 10_000_000);
227 assert!(sp.done.load(Ordering::Relaxed));
228 unset_no_color();
229 }
230}