1use crate::terminal::{Terminal, Verbosity};
7use anyhow::Result;
8use owo_colors::OwoColorize;
9use std::io::{stderr, Write};
10use std::time::{Duration, Instant};
11use std::{cmp, fmt};
12use unicode_width::UnicodeWidthChar;
13
14fn is_ci() -> bool {
15 std::env::var("CI").is_ok() || std::env::var("TF_BUILD").is_ok()
16}
17
18pub struct ProgressBar<'a> {
20 state: Option<State<'a>>,
21}
22
23pub enum ProgressStyle {
27 Percentage,
33 Ratio,
39 Indeterminate,
46}
47
48struct Throttle {
49 first: bool,
50 last_update: Instant,
51}
52
53struct State<'a> {
54 terminal: &'a Terminal,
55 format: Format,
56 name: String,
57 done: bool,
58 throttle: Throttle,
59 last_line: Option<String>,
60}
61
62struct Format {
63 style: ProgressStyle,
64 max_width: usize,
65 max_print: usize,
66}
67
68impl<'a> ProgressBar<'a> {
69 pub fn with_style(name: &str, style: ProgressStyle, terminal: &'a Terminal) -> Self {
80 let dumb = match std::env::var("TERM") {
84 Ok(term) => term == "dumb",
85 Err(_) => false,
86 };
87
88 let verbosity = terminal.verbosity();
89 if verbosity == Verbosity::Quiet || dumb || is_ci() {
90 return Self { state: None };
91 }
92
93 Self::new_priv(name, style, terminal)
94 }
95
96 fn new_priv(name: &str, style: ProgressStyle, terminal: &'a Terminal) -> Self {
97 let width = terminal.width();
98
99 Self {
100 state: width.map(|n| State {
101 terminal,
102 format: Format {
103 style,
104 max_width: n,
105 max_print: 50,
108 },
109 name: name.to_string(),
110 done: false,
111 throttle: Throttle::new(),
112 last_line: None,
113 }),
114 }
115 }
116
117 pub fn disable(&mut self) {
119 self.state = None;
120 }
121
122 pub fn is_enabled(&self) -> bool {
124 self.state.is_some()
125 }
126
127 pub fn new(name: &str, terminal: &'a Terminal) -> Self {
131 Self::with_style(name, ProgressStyle::Percentage, terminal)
132 }
133
134 pub fn tick(&mut self, cur: usize, max: usize, msg: &str) -> Result<()> {
145 let s = match &mut self.state {
146 Some(s) => s,
147 None => return Ok(()),
148 };
149
150 if !s.throttle.allowed() {
163 return Ok(());
164 }
165
166 s.tick(cur, max, msg)
167 }
168
169 pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> Result<()> {
178 match self.state {
179 Some(ref mut s) => s.tick(cur, max, msg),
180 None => Ok(()),
181 }
182 }
183
184 pub fn update_allowed(&mut self) -> bool {
189 match &mut self.state {
190 Some(s) => s.throttle.allowed(),
191 None => false,
192 }
193 }
194
195 pub fn print_now(&mut self, msg: &str) -> Result<()> {
204 match &mut self.state {
205 Some(s) => s.print("", msg),
206 None => Ok(()),
207 }
208 }
209
210 pub fn clear(&mut self) {
212 if let Some(ref mut s) = self.state {
213 s.clear();
214 }
215 }
216}
217
218impl Throttle {
219 fn new() -> Throttle {
220 Throttle {
221 first: true,
222 last_update: Instant::now(),
223 }
224 }
225
226 fn allowed(&mut self) -> bool {
227 if self.first {
228 let delay = Duration::from_millis(500);
229 if self.last_update.elapsed() < delay {
230 return false;
231 }
232 } else {
233 let interval = Duration::from_millis(100);
234 if self.last_update.elapsed() < interval {
235 return false;
236 }
237 }
238 self.update();
239 true
240 }
241
242 fn update(&mut self) {
243 self.first = false;
244 self.last_update = Instant::now();
245 }
246}
247
248impl<'a> State<'a> {
249 fn tick(&mut self, cur: usize, max: usize, msg: &str) -> Result<()> {
250 if self.done {
251 return Ok(());
252 }
253
254 if max > 0 && cur == max {
255 self.done = true;
256 }
257
258 self.try_update_max_width();
261 if let Some(pbar) = self.format.progress(cur, max) {
262 self.print(&pbar, msg)?;
263 }
264 Ok(())
265 }
266
267 fn print(&mut self, prefix: &str, msg: &str) -> Result<()> {
268 self.throttle.update();
269 self.try_update_max_width();
270
271 if self.format.max_width < 15 {
273 return Ok(());
274 }
275
276 let mut line = prefix.to_string();
277 self.format.render(&mut line, msg);
278 while line.len() < self.format.max_width - 15 {
279 line.push(' ');
280 }
281
282 let mut state = self.terminal.state_mut();
283
284 if !state.needs_clear || self.last_line.as_ref() != Some(&line) {
286 let name_cyan = self.name.cyan();
287
288 let status = if state.output.supports_color() {
289 &name_cyan as &dyn fmt::Display
290 } else {
291 &self.name
292 };
293
294 state.output.print(status, None, true)?;
295 write!(&mut stderr(), "{line}\r")?;
296 self.last_line = Some(line);
297 state.needs_clear = true;
298 }
299
300 Ok(())
301 }
302
303 fn clear(&mut self) {
304 if self.last_line.is_some() {
306 self.terminal.state_mut().clear_stderr();
307 self.last_line = None;
308 }
309 }
310
311 fn try_update_max_width(&mut self) {
312 if let Some(width) = self.terminal.width() {
313 self.format.max_width = width;
314 }
315 }
316}
317
318impl Format {
319 fn progress(&self, cur: usize, max: usize) -> Option<String> {
320 assert!(cur <= max);
321 let pct = (cur as f64) / (max as f64);
324 let pct = if !pct.is_finite() { 0.0 } else { pct };
325 let stats = match self.style {
326 ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
327 ProgressStyle::Ratio => format!(" {}/{}", cur, max),
328 ProgressStyle::Indeterminate => String::new(),
329 };
330 let extra_len = stats.len() + 2 + 15 ;
331 let display_width = match self.width().checked_sub(extra_len) {
332 Some(n) => n,
333 None => return None,
334 };
335
336 let mut string = String::with_capacity(self.max_width);
337 string.push('[');
338 let hashes = display_width as f64 * pct;
339 let hashes = hashes as usize;
340
341 if hashes > 0 {
343 for _ in 0..hashes - 1 {
344 string.push('=');
345 }
346 if cur == max {
347 string.push('=');
348 } else {
349 string.push('>');
350 }
351 }
352
353 for _ in 0..(display_width - hashes) {
355 string.push(' ');
356 }
357 string.push(']');
358 string.push_str(&stats);
359
360 Some(string)
361 }
362
363 fn render(&self, string: &mut String, msg: &str) {
364 let mut avail_msg_len = self.max_width - string.len() - 15;
365 let mut ellipsis_pos = 0;
366 if avail_msg_len <= 3 {
367 return;
368 }
369 for c in msg.chars() {
370 let display_width = c.width().unwrap_or(0);
371 if avail_msg_len >= display_width {
372 avail_msg_len -= display_width;
373 string.push(c);
374 if avail_msg_len >= 3 {
375 ellipsis_pos = string.len();
376 }
377 } else {
378 string.truncate(ellipsis_pos);
379 string.push_str("...");
380 break;
381 }
382 }
383 }
384
385 #[cfg(test)]
386 fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
387 let mut ret = self.progress(cur, max)?;
388 self.render(&mut ret, msg);
389 Some(ret)
390 }
391
392 fn width(&self) -> usize {
393 cmp::min(self.max_width, self.max_print)
394 }
395}
396
397impl<'a> Drop for State<'a> {
398 fn drop(&mut self) {
399 self.clear();
400 }
401}
402
403#[cfg(test)]
404mod test {
405 use super::*;
406
407 #[test]
408 fn test_progress_status() {
409 let format = Format {
410 style: ProgressStyle::Ratio,
411 max_print: 40,
412 max_width: 60,
413 };
414 assert_eq!(
415 format.progress_status(0, 4, ""),
416 Some("[ ] 0/4".to_string())
417 );
418 assert_eq!(
419 format.progress_status(1, 4, ""),
420 Some("[===> ] 1/4".to_string())
421 );
422 assert_eq!(
423 format.progress_status(2, 4, ""),
424 Some("[========> ] 2/4".to_string())
425 );
426 assert_eq!(
427 format.progress_status(3, 4, ""),
428 Some("[=============> ] 3/4".to_string())
429 );
430 assert_eq!(
431 format.progress_status(4, 4, ""),
432 Some("[===================] 4/4".to_string())
433 );
434
435 assert_eq!(
436 format.progress_status(3999, 4000, ""),
437 Some("[===========> ] 3999/4000".to_string())
438 );
439 assert_eq!(
440 format.progress_status(4000, 4000, ""),
441 Some("[=============] 4000/4000".to_string())
442 );
443
444 assert_eq!(
445 format.progress_status(3, 4, ": short message"),
446 Some("[=============> ] 3/4: short message".to_string())
447 );
448 assert_eq!(
449 format.progress_status(3, 4, ": msg thats just fit"),
450 Some("[=============> ] 3/4: msg thats just fit".to_string())
451 );
452 assert_eq!(
453 format.progress_status(3, 4, ": msg that's just fit"),
454 Some("[=============> ] 3/4: msg that's just...".to_string())
455 );
456
457 let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
459 assert_eq!(
460 format.progress_status(3, 4, zalgo_msg),
461 Some("[=============> ] 3/4".to_string() + zalgo_msg)
462 );
463
464 assert_eq!(
466 format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
467 Some("[=============> ] 3/4_123456789123456e\u{301}\u{301}...".to_string())
468 );
469 assert_eq!(
470 format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
471 Some("[=============> ] 3/4:每個漢字佔據了...".to_string())
472 );
473 assert_eq!(
474 format.progress_status(3, 4, ":-每個漢字佔據了兩個字元"),
476 Some("[=============> ] 3/4:-每個漢字佔據了...".to_string())
477 );
478 }
479
480 #[test]
481 fn test_progress_status_percentage() {
482 let format = Format {
483 style: ProgressStyle::Percentage,
484 max_print: 40,
485 max_width: 60,
486 };
487 assert_eq!(
488 format.progress_status(0, 77, ""),
489 Some("[ ] 0.00%".to_string())
490 );
491 assert_eq!(
492 format.progress_status(1, 77, ""),
493 Some("[ ] 1.30%".to_string())
494 );
495 assert_eq!(
496 format.progress_status(76, 77, ""),
497 Some("[=============> ] 98.70%".to_string())
498 );
499 assert_eq!(
500 format.progress_status(77, 77, ""),
501 Some("[===============] 100.00%".to_string())
502 );
503 }
504
505 #[test]
506 fn test_progress_status_too_short() {
507 let format = Format {
508 style: ProgressStyle::Percentage,
509 max_print: 25,
510 max_width: 25,
511 };
512 assert_eq!(
513 format.progress_status(1, 1, ""),
514 Some("[] 100.00%".to_string())
515 );
516
517 let format = Format {
518 style: ProgressStyle::Percentage,
519 max_print: 24,
520 max_width: 24,
521 };
522 assert_eq!(format.progress_status(1, 1, ""), None);
523 }
524}