1use std::cmp;
2use std::env;
3use std::time::{Duration, Instant};
4
5use crate::core::shell::Verbosity;
6use crate::util::{is_ci, CargoResult, Config};
7
8use unicode_width::UnicodeWidthChar;
9
10pub struct Progress<'cfg> {
11 state: Option<State<'cfg>>,
12}
13
14pub enum ProgressStyle {
15 Percentage,
16 Ratio,
17}
18
19struct Throttle {
20 first: bool,
21 last_update: Instant,
22}
23
24struct State<'cfg> {
25 config: &'cfg Config,
26 format: Format,
27 name: String,
28 done: bool,
29 throttle: Throttle,
30 last_line: Option<String>,
31}
32
33struct Format {
34 style: ProgressStyle,
35 max_width: usize,
36 max_print: usize,
37}
38
39impl<'cfg> Progress<'cfg> {
40 pub fn with_style(name: &str, style: ProgressStyle, cfg: &'cfg Config) -> Progress<'cfg> {
41 let dumb = match env::var("TERM") {
45 Ok(term) => term == "dumb",
46 Err(_) => false,
47 };
48 if cfg.shell().verbosity() == Verbosity::Quiet || dumb || is_ci() {
49 return Progress { state: None };
50 }
51
52 Progress {
53 state: cfg.shell().err_width().map(|n| State {
54 config: cfg,
55 format: Format {
56 style,
57 max_width: n,
58 max_print: 80,
59 },
60 name: name.to_string(),
61 done: false,
62 throttle: Throttle::new(),
63 last_line: None,
64 }),
65 }
66 }
67
68 pub fn disable(&mut self) {
69 self.state = None;
70 }
71
72 pub fn is_enabled(&self) -> bool {
73 self.state.is_some()
74 }
75
76 pub fn new(name: &str, cfg: &'cfg Config) -> Progress<'cfg> {
77 Self::with_style(name, ProgressStyle::Percentage, cfg)
78 }
79
80 pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
81 let s = match &mut self.state {
82 Some(s) => s,
83 None => return Ok(()),
84 };
85
86 if !s.throttle.allowed() {
99 return Ok(());
100 }
101
102 s.tick(cur, max, "")
103 }
104
105 pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
106 match self.state {
107 Some(ref mut s) => s.tick(cur, max, msg),
108 None => Ok(()),
109 }
110 }
111
112 pub fn update_allowed(&mut self) -> bool {
113 match &mut self.state {
114 Some(s) => s.throttle.allowed(),
115 None => false,
116 }
117 }
118
119 pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
120 match &mut self.state {
121 Some(s) => s.print("", msg),
122 None => Ok(()),
123 }
124 }
125
126 pub fn clear(&mut self) {
127 if let Some(ref mut s) = self.state {
128 s.clear();
129 }
130 }
131}
132
133impl Throttle {
134 fn new() -> Throttle {
135 Throttle {
136 first: true,
137 last_update: Instant::now(),
138 }
139 }
140
141 fn allowed(&mut self) -> bool {
142 if self.first {
143 let delay = Duration::from_millis(500);
144 if self.last_update.elapsed() < delay {
145 return false;
146 }
147 } else {
148 let interval = Duration::from_millis(100);
149 if self.last_update.elapsed() < interval {
150 return false;
151 }
152 }
153 self.update();
154 true
155 }
156
157 fn update(&mut self) {
158 self.first = false;
159 self.last_update = Instant::now();
160 }
161}
162
163impl<'cfg> State<'cfg> {
164 fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
165 if self.done {
166 return Ok(());
167 }
168
169 if max > 0 && cur == max {
170 self.done = true;
171 }
172
173 self.try_update_max_width();
176 if let Some(pbar) = self.format.progress(cur, max) {
177 self.print(&pbar, msg)?;
178 }
179 Ok(())
180 }
181
182 fn print(&mut self, prefix: &str, msg: &str) -> CargoResult<()> {
183 self.throttle.update();
184 self.try_update_max_width();
185
186 if self.format.max_width < 15 {
188 return Ok(());
189 }
190
191 let mut line = prefix.to_string();
192 self.format.render(&mut line, msg);
193 while line.len() < self.format.max_width - 15 {
194 line.push(' ');
195 }
196
197 if self.config.shell().is_cleared() || self.last_line.as_ref() != Some(&line) {
199 let mut shell = self.config.shell();
200 shell.set_needs_clear(false);
201 shell.status_header(&self.name)?;
202 write!(shell.err(), "{}\r", line)?;
203 self.last_line = Some(line);
204 shell.set_needs_clear(true);
205 }
206
207 Ok(())
208 }
209
210 fn clear(&mut self) {
211 if self.last_line.is_some() && !self.config.shell().is_cleared() {
213 self.config.shell().err_erase_line();
214 self.last_line = None;
215 }
216 }
217
218 fn try_update_max_width(&mut self) {
219 if let Some(n) = self.config.shell().err_width() {
220 self.format.max_width = n;
221 }
222 }
223}
224
225impl Format {
226 fn progress(&self, cur: usize, max: usize) -> Option<String> {
227 assert!(cur <= max);
228 let pct = (cur as f64) / (max as f64);
231 let pct = if !pct.is_finite() { 0.0 } else { pct };
232 let stats = match self.style {
233 ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
234 ProgressStyle::Ratio => format!(" {}/{}", cur, max),
235 };
236 let extra_len = stats.len() + 2 + 15 ;
237 let display_width = match self.width().checked_sub(extra_len) {
238 Some(n) => n,
239 None => return None,
240 };
241
242 let mut string = String::with_capacity(self.max_width);
243 string.push('[');
244 let hashes = display_width as f64 * pct;
245 let hashes = hashes as usize;
246
247 if hashes > 0 {
249 for _ in 0..hashes - 1 {
250 string.push_str("=");
251 }
252 if cur == max {
253 string.push_str("=");
254 } else {
255 string.push_str(">");
256 }
257 }
258
259 for _ in 0..(display_width - hashes) {
261 string.push_str(" ");
262 }
263 string.push_str("]");
264 string.push_str(&stats);
265
266 Some(string)
267 }
268
269 fn render(&self, string: &mut String, msg: &str) {
270 let mut avail_msg_len = self.max_width - string.len() - 15;
271 let mut ellipsis_pos = 0;
272 if avail_msg_len <= 3 {
273 return;
274 }
275 for c in msg.chars() {
276 let display_width = c.width().unwrap_or(0);
277 if avail_msg_len >= display_width {
278 avail_msg_len -= display_width;
279 string.push(c);
280 if avail_msg_len >= 3 {
281 ellipsis_pos = string.len();
282 }
283 } else {
284 string.truncate(ellipsis_pos);
285 string.push_str("...");
286 break;
287 }
288 }
289 }
290
291 #[cfg(test)]
292 fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
293 let mut ret = self.progress(cur, max)?;
294 self.render(&mut ret, msg);
295 Some(ret)
296 }
297
298 fn width(&self) -> usize {
299 cmp::min(self.max_width, self.max_print)
300 }
301}
302
303impl<'cfg> Drop for State<'cfg> {
304 fn drop(&mut self) {
305 self.clear();
306 }
307}
308
309#[test]
310fn test_progress_status() {
311 let format = Format {
312 style: ProgressStyle::Ratio,
313 max_print: 40,
314 max_width: 60,
315 };
316 assert_eq!(
317 format.progress_status(0, 4, ""),
318 Some("[ ] 0/4".to_string())
319 );
320 assert_eq!(
321 format.progress_status(1, 4, ""),
322 Some("[===> ] 1/4".to_string())
323 );
324 assert_eq!(
325 format.progress_status(2, 4, ""),
326 Some("[========> ] 2/4".to_string())
327 );
328 assert_eq!(
329 format.progress_status(3, 4, ""),
330 Some("[=============> ] 3/4".to_string())
331 );
332 assert_eq!(
333 format.progress_status(4, 4, ""),
334 Some("[===================] 4/4".to_string())
335 );
336
337 assert_eq!(
338 format.progress_status(3999, 4000, ""),
339 Some("[===========> ] 3999/4000".to_string())
340 );
341 assert_eq!(
342 format.progress_status(4000, 4000, ""),
343 Some("[=============] 4000/4000".to_string())
344 );
345
346 assert_eq!(
347 format.progress_status(3, 4, ": short message"),
348 Some("[=============> ] 3/4: short message".to_string())
349 );
350 assert_eq!(
351 format.progress_status(3, 4, ": msg thats just fit"),
352 Some("[=============> ] 3/4: msg thats just fit".to_string())
353 );
354 assert_eq!(
355 format.progress_status(3, 4, ": msg that's just fit"),
356 Some("[=============> ] 3/4: msg that's just...".to_string())
357 );
358
359 let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
361 assert_eq!(
362 format.progress_status(3, 4, zalgo_msg),
363 Some("[=============> ] 3/4".to_string() + zalgo_msg)
364 );
365
366 assert_eq!(
368 format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
369 Some("[=============> ] 3/4_123456789123456e\u{301}\u{301}...".to_string())
370 );
371 assert_eq!(
372 format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
373 Some("[=============> ] 3/4:每個漢字佔據了...".to_string())
374 );
375}
376
377#[test]
378fn test_progress_status_percentage() {
379 let format = Format {
380 style: ProgressStyle::Percentage,
381 max_print: 40,
382 max_width: 60,
383 };
384 assert_eq!(
385 format.progress_status(0, 77, ""),
386 Some("[ ] 0.00%".to_string())
387 );
388 assert_eq!(
389 format.progress_status(1, 77, ""),
390 Some("[ ] 1.30%".to_string())
391 );
392 assert_eq!(
393 format.progress_status(76, 77, ""),
394 Some("[=============> ] 98.70%".to_string())
395 );
396 assert_eq!(
397 format.progress_status(77, 77, ""),
398 Some("[===============] 100.00%".to_string())
399 );
400}
401
402#[test]
403fn test_progress_status_too_short() {
404 let format = Format {
405 style: ProgressStyle::Percentage,
406 max_print: 25,
407 max_width: 25,
408 };
409 assert_eq!(
410 format.progress_status(1, 1, ""),
411 Some("[] 100.00%".to_string())
412 );
413
414 let format = Format {
415 style: ProgressStyle::Percentage,
416 max_print: 24,
417 max_width: 24,
418 };
419 assert_eq!(format.progress_status(1, 1, ""), None);
420}