1use crate::output::hook;
2use crossterm::style::{Color, Stylize};
3use std::io::{stdout, Write};
4use std::thread;
5use std::time::{Duration, Instant};
6
7#[derive(Clone)]
9pub struct ProgressStyle {
10 pub fill: char,
11 pub start_cap: char,
12 pub end_cap: char,
13 pub done_label: &'static str,
14 pub show_percent: bool,
15 pub color: Option<Color>,
16}
17
18impl Default for MultiProgress {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24pub struct MultiProgress {
29 bars: Vec<ProgressBar>,
30}
31
32impl MultiProgress {
33 pub fn new() -> Self {
34 Self { bars: Vec::new() }
35 }
36
37 pub fn add_bar(&mut self, label: &str, total_steps: usize, style: ProgressStyle) -> usize {
39 let mut bar = ProgressBar::new(total_steps, style);
40 bar.set_label(label);
41 if bar.start_time.is_none() {
42 bar.start_time = Some(Instant::now());
43 }
44 self.bars.push(bar);
45 self.bars.len() - 1
46 }
47
48 pub fn get_bar_mut(&mut self, idx: usize) -> Option<&mut ProgressBar> {
49 self.bars.get_mut(idx)
50 }
51
52 pub fn tick(&mut self, idx: usize) {
53 if let Some(b) = self.bars.get_mut(idx) {
54 b.tick();
55 }
56 }
57 pub fn set_progress(&mut self, idx: usize, value: usize) {
58 if let Some(b) = self.bars.get_mut(idx) {
59 b.set_progress(value);
60 }
61 }
62 pub fn set_bytes_processed(&mut self, idx: usize, bytes: u64) {
63 if let Some(b) = self.bars.get_mut(idx) {
64 b.set_bytes_processed(bytes);
65 }
66 }
67
68 pub fn refresh(&mut self) {
70 let n = self.bars.len();
71 if n == 0 {
72 return;
73 }
74 print!("\x1B[{n}A"); for b in &self.bars {
77 b.render();
78 println!();
79 }
80 let _ = stdout().flush();
81 }
82
83 pub fn finish(&mut self) {
85 for b in &self.bars {
86 b.render();
88 println!(" {}", b.style.done_label);
89 }
90 let _ = stdout().flush();
91 }
92}
93
94impl Default for ProgressStyle {
95 fn default() -> Self {
96 Self {
97 fill: '#',
98 start_cap: '[',
99 end_cap: ']',
100 done_label: "Done!",
101 show_percent: true,
102 color: None,
103 }
104 }
105}
106
107fn human_bytes_per_sec(bps: f64) -> String {
110 let abs = bps.abs();
111 const K: f64 = 1024.0;
112 let (value, unit) = if abs >= K * K * K {
113 (bps / (K * K * K), "GiB/s")
114 } else if abs >= K * K {
115 (bps / (K * K), "MiB/s")
116 } else if abs >= K {
117 (bps / K, "KiB/s")
118 } else {
119 (bps, "B/s")
120 };
121 if value.abs() >= 100.0 {
122 format!("{value:>4.0} {unit}")
123 } else {
124 format!("{value:>4.1} {unit}")
125 }
126}
127
128fn human_duration(d: Duration) -> String {
129 let mut secs = d.as_secs();
130 let h = secs / 3600;
131 secs %= 3600;
132 let m = secs / 60;
133 let s = secs % 60;
134 if h > 0 {
135 format!("{h:02}:{m:02}:{s:02}")
136 } else {
137 format!("{m:02}:{s:02}")
138 }
139}
140
141pub struct ProgressBar {
143 pub total_steps: usize,
144 pub current: usize,
145 pub label: Option<String>,
146 pub style: ProgressStyle,
147 pub total_bytes: Option<u64>,
149 pub bytes_processed: u64,
150 start_time: Option<Instant>,
152 last_tick: Option<Instant>,
153 paused: bool,
155}
156
157impl ProgressBar {
158 pub fn new(total_steps: usize, style: ProgressStyle) -> Self {
159 Self {
160 total_steps,
161 current: 0,
162 label: None,
163 style,
164 total_bytes: None,
165 bytes_processed: 0,
166 start_time: None,
167 last_tick: None,
168 paused: false,
169 }
170 }
171
172 pub fn set_label(&mut self, label: &str) {
173 self.label = Some(label.to_string());
174 }
175
176 pub fn set_progress(&mut self, value: usize) {
177 self.current = value.min(self.total_steps);
178 if self.start_time.is_none() {
179 self.start_time = Some(Instant::now());
180 }
181 self.last_tick = Some(Instant::now());
182 self.render();
183 }
184
185 pub fn tick(&mut self) {
186 if self.paused {
187 return;
188 }
189 self.current += 1;
190 if self.current > self.total_steps {
191 self.current = self.total_steps;
192 }
193 if self.start_time.is_none() {
194 self.start_time = Some(Instant::now());
195 }
196 self.last_tick = Some(Instant::now());
197 self.render();
198 }
199
200 pub fn set_bytes_total(&mut self, total: u64) {
202 self.total_bytes = Some(total);
203 }
204
205 pub fn set_bytes_processed(&mut self, processed: u64) {
207 self.bytes_processed = processed;
208 if self.start_time.is_none() {
209 self.start_time = Some(Instant::now());
210 }
211 self.last_tick = Some(Instant::now());
212 self.render();
213 }
214
215 pub fn set_bytes(&mut self, total: u64, processed: u64) {
217 self.total_bytes = Some(total);
218 self.set_bytes_processed(processed);
219 }
220
221 pub fn pause(&mut self) {
223 self.paused = true;
224 }
225 pub fn resume(&mut self) {
227 self.paused = false;
228 self.last_tick = Some(Instant::now());
229 }
230
231 pub fn start_auto(&mut self, duration_ms: u64) {
232 let interval = duration_ms / self.total_steps.max(1) as u64;
233 for _ in 0..self.total_steps {
234 self.tick();
235 thread::sleep(Duration::from_millis(interval));
236 }
237 println!(" {}", self.style.done_label);
238 }
239
240 fn render(&self) {
241 let mut percent_val: usize = self.current * 100 / self.total_steps.max(1);
242
243 if let Some(total) = self.total_bytes {
245 if total > 0 {
246 percent_val = ((self.bytes_processed.saturating_mul(100)) / total.max(1)) as usize;
247 }
248 }
249
250 let percent = if self.style.show_percent {
251 format!(" {:>3}%", percent_val.min(100))
252 } else {
253 String::new()
254 };
255
256 let fill_from_percent =
258 |pct: usize, width: usize| -> usize { ((pct.min(100) * width) / 100).min(width) };
259
260 let (fill_count, empty_count) = if self.total_bytes.is_some() {
261 let fill = fill_from_percent(percent_val, self.total_steps);
262 (fill, self.total_steps - fill)
263 } else {
264 (self.current, self.total_steps - self.current)
265 };
266
267 let mut bar = format!(
268 "{}{}{}{}",
269 self.style.start_cap,
270 self.style.fill.to_string().repeat(fill_count),
271 " ".repeat(empty_count),
272 self.style.end_cap
273 );
274
275 if let Some(color) = self.style.color {
276 bar = bar.with(color).to_string();
277 }
278 print!("\r");
279
280 if let Some(ref label) = self.label {
281 print!("{label} {bar}");
282 } else {
283 print!("{bar}");
284 }
285
286 if let Some(total) = self.total_bytes {
288 let elapsed = self.start_time.map(|t| t.elapsed()).unwrap_or_default();
289 let rate_bps = if elapsed.as_secs_f64() > 0.0 {
290 self.bytes_processed as f64 / elapsed.as_secs_f64()
291 } else {
292 0.0
293 };
294 let remaining = total.saturating_sub(self.bytes_processed);
295 let eta_secs = if rate_bps > 0.0 {
296 (remaining as f64 / rate_bps).round() as u64
297 } else {
298 0
299 };
300
301 let rate_str = human_bytes_per_sec(rate_bps);
302 let eta_str = human_duration(Duration::from_secs(eta_secs));
303 print!("{percent} {rate_str} ETA {eta_str}");
304 } else {
305 print!("{percent}");
306 }
307
308 if let Err(e) = stdout().flush() {
309 hook::warn(&format!("flush failed: {e}"));
310 }
311 }
312}
313
314pub fn show_progress_bar(label: &str, total_steps: usize, duration_ms: u64) {
317 let mut bar = ProgressBar::new(total_steps, ProgressStyle::default());
318 bar.set_label(label);
319 bar.start_auto(duration_ms);
320}
321
322pub fn show_percent_progress(label: &str, percent: usize) {
323 let clamped = percent.clamp(0, 100);
324 print!("\r{label}: {clamped:>3}% complete");
325 if let Err(e) = stdout().flush() {
326 hook::warn(&format!("flush failed: {e}"));
327 }
328}
329
330pub fn show_spinner(label: &str, cycles: usize, delay_ms: u64) {
331 let spinner = ['|', '/', '-', '\\'];
332 let mut stdout = stdout();
333 print!("{label} ");
334
335 for i in 0..cycles {
336 let frame = spinner[i % spinner.len()];
337 print!("\r{label} {frame}");
338 if let Err(e) = stdout.flush() {
339 hook::warn(&format!("flush failed: {e}"));
340 }
341 thread::sleep(Duration::from_millis(delay_ms));
342 }
343
344 println!("{label} β");
345}
346
347pub fn show_emoji_spinner(label: &str, cycles: usize, delay_ms: u64) {
349 const FRAMES: [&str; 8] = ["π", "π", "π", "π", "π", "π", "π", "π"];
350 let mut stdout = stdout();
351 print!("{label} ");
352
353 for i in 0..cycles {
354 let frame = FRAMES[i % FRAMES.len()];
355 print!("\r{label} {frame}");
356 if let Err(e) = stdout.flush() {
357 hook::warn(&format!("flush failed: {e}"));
358 }
359 thread::sleep(Duration::from_millis(delay_ms));
360 }
361
362 println!("{label} β
");
363}