]> git.proxmox.com Git - cargo.git/blob - src/cargo/util/progress.rs
fe90d311914e4245ece7f10b9a56596522c1dd49
[cargo.git] / src / cargo / util / progress.rs
1 use std::cmp;
2 use std::env;
3 use std::time::{Duration, Instant};
4
5 use core::shell::Verbosity;
6 use util::{CargoResult, Config};
7
8 use unicode_width::UnicodeWidthChar;
9
10 pub struct Progress<'cfg> {
11 state: Option<State<'cfg>>,
12 }
13
14 pub enum ProgressStyle {
15 Percentage,
16 Ratio,
17 }
18
19 struct Throttle {
20 first: bool,
21 last_update: Instant,
22 }
23
24 struct State<'cfg> {
25 config: &'cfg Config,
26 format: Format,
27 name: String,
28 done: bool,
29 throttle: Throttle,
30 }
31
32 struct Format {
33 style: ProgressStyle,
34 max_width: usize,
35 max_print: usize,
36 }
37
38 impl<'cfg> Progress<'cfg> {
39 pub fn with_style(name: &str, style: ProgressStyle, cfg: &'cfg Config) -> Progress<'cfg> {
40 // report no progress when -q (for quiet) or TERM=dumb are set
41 // or if running on Continuous Integration service like Travis where the
42 // output logs get mangled.
43 let dumb = match env::var("TERM") {
44 Ok(term) => term == "dumb",
45 Err(_) => false,
46 };
47 if cfg.shell().verbosity() == Verbosity::Quiet || dumb || env::var("CI").is_ok() {
48 return Progress { state: None };
49 }
50
51 Progress {
52 state: cfg.shell().err_width().map(|n| State {
53 config: cfg,
54 format: Format {
55 style,
56 max_width: n,
57 max_print: 80,
58 },
59 name: name.to_string(),
60 done: false,
61 throttle: Throttle::new(),
62 }),
63 }
64 }
65
66 pub fn disable(&mut self) {
67 self.state = None;
68 }
69
70 pub fn is_enabled(&self) -> bool {
71 self.state.is_some()
72 }
73
74 pub fn new(name: &str, cfg: &'cfg Config) -> Progress<'cfg> {
75 Self::with_style(name, ProgressStyle::Percentage, cfg)
76 }
77
78 pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
79 let s = match &mut self.state {
80 Some(s) => s,
81 None => return Ok(()),
82 };
83
84 // Don't update too often as it can cause excessive performance loss
85 // just putting stuff onto the terminal. We also want to avoid
86 // flickering by not drawing anything that goes away too quickly. As a
87 // result we've got two branches here:
88 //
89 // 1. If we haven't drawn anything, we wait for a period of time to
90 // actually start drawing to the console. This ensures that
91 // short-lived operations don't flicker on the console. Currently
92 // there's a 500ms delay to when we first draw something.
93 // 2. If we've drawn something, then we rate limit ourselves to only
94 // draw to the console every so often. Currently there's a 100ms
95 // delay between updates.
96 if !s.throttle.allowed() {
97 return Ok(())
98 }
99
100 s.tick(cur, max, "")
101 }
102
103 pub fn tick_now(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
104 match self.state {
105 Some(ref mut s) => s.tick(cur, max, msg),
106 None => Ok(()),
107 }
108 }
109
110 pub fn update_allowed(&mut self) -> bool {
111 match &mut self.state {
112 Some(s) => s.throttle.allowed(),
113 None => false,
114 }
115 }
116
117 pub fn print_now(&mut self, msg: &str) -> CargoResult<()> {
118 match &mut self.state {
119 Some(s) => s.print("", msg),
120 None => Ok(()),
121 }
122 }
123
124 pub fn clear(&mut self) {
125 if let Some(ref mut s) = self.state {
126 s.clear();
127 }
128 }
129 }
130
131 impl Throttle {
132 fn new() -> Throttle {
133 Throttle {
134 first: true,
135 last_update: Instant::now(),
136 }
137 }
138
139 fn allowed(&mut self) -> bool {
140 if self.first {
141 let delay = Duration::from_millis(500);
142 if self.last_update.elapsed() < delay {
143 return false
144 }
145 } else {
146 let interval = Duration::from_millis(100);
147 if self.last_update.elapsed() < interval {
148 return false
149 }
150 }
151 self.update();
152 true
153 }
154
155 fn update(&mut self) {
156 self.first = false;
157 self.last_update = Instant::now();
158 }
159 }
160
161 impl<'cfg> State<'cfg> {
162 fn tick(&mut self, cur: usize, max: usize, msg: &str) -> CargoResult<()> {
163 if self.done {
164 return Ok(());
165 }
166
167 if max > 0 && cur == max {
168 self.done = true;
169 }
170
171 // Write out a pretty header, then the progress bar itself, and then
172 // return back to the beginning of the line for the next print.
173 self.try_update_max_width();
174 if let Some(pbar) = self.format.progress(cur, max) {
175 self.print(&pbar, msg)?;
176 }
177 Ok(())
178 }
179
180 fn print(&mut self, prefix: &str, msg: &str) -> CargoResult<()> {
181 self.throttle.update();
182 self.try_update_max_width();
183
184 // make sure we have enough room for the header
185 if self.format.max_width < 15 {
186 return Ok(())
187 }
188 self.config.shell().status_header(&self.name)?;
189 let mut line = prefix.to_string();
190 self.format.render(&mut line, msg);
191
192 while line.len() < self.format.max_width - 15 {
193 line.push(' ');
194 }
195
196 write!(self.config.shell().err(), "{}\r", line)?;
197 Ok(())
198 }
199
200 fn clear(&mut self) {
201 self.config.shell().err_erase_line();
202 }
203
204 fn try_update_max_width(&mut self) {
205 if let Some(n) = self.config.shell().err_width() {
206 self.format.max_width = n;
207 }
208 }
209 }
210
211 impl Format {
212 fn progress(&self, cur: usize, max: usize) -> Option<String> {
213 // Render the percentage at the far right and then figure how long the
214 // progress bar is
215 let pct = (cur as f64) / (max as f64);
216 let pct = if !pct.is_finite() { 0.0 } else { pct };
217 let stats = match self.style {
218 ProgressStyle::Percentage => format!(" {:6.02}%", pct * 100.0),
219 ProgressStyle::Ratio => format!(" {}/{}", cur, max),
220 };
221 let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
222 let display_width = match self.width().checked_sub(extra_len) {
223 Some(n) => n,
224 None => return None,
225 };
226
227 let mut string = String::with_capacity(self.max_width);
228 string.push('[');
229 let hashes = display_width as f64 * pct;
230 let hashes = hashes as usize;
231
232 // Draw the `===>`
233 if hashes > 0 {
234 for _ in 0..hashes - 1 {
235 string.push_str("=");
236 }
237 if cur == max {
238 string.push_str("=");
239 } else {
240 string.push_str(">");
241 }
242 }
243
244 // Draw the empty space we have left to do
245 for _ in 0..(display_width - hashes) {
246 string.push_str(" ");
247 }
248 string.push_str("]");
249 string.push_str(&stats);
250
251 Some(string)
252 }
253
254 fn render(&self, string: &mut String, msg: &str) {
255 let mut avail_msg_len = self.max_width - string.len() - 15;
256 let mut ellipsis_pos = 0;
257 if avail_msg_len <= 3 {
258 return
259 }
260 for c in msg.chars() {
261 let display_width = c.width().unwrap_or(0);
262 if avail_msg_len >= display_width {
263 avail_msg_len -= display_width;
264 string.push(c);
265 if avail_msg_len >= 3 {
266 ellipsis_pos = string.len();
267 }
268 } else {
269 string.truncate(ellipsis_pos);
270 string.push_str("...");
271 break;
272 }
273 }
274 }
275
276 #[cfg(test)]
277 fn progress_status(&self, cur: usize, max: usize, msg: &str) -> Option<String> {
278 let mut ret = self.progress(cur, max)?;
279 self.render(&mut ret, msg);
280 Some(ret)
281 }
282
283 fn width(&self) -> usize {
284 cmp::min(self.max_width, self.max_print)
285 }
286 }
287
288 impl<'cfg> Drop for State<'cfg> {
289 fn drop(&mut self) {
290 self.clear();
291 }
292 }
293
294 #[test]
295 fn test_progress_status() {
296 let format = Format {
297 style: ProgressStyle::Ratio,
298 max_print: 40,
299 max_width: 60,
300 };
301 assert_eq!(
302 format.progress_status(0, 4, ""),
303 Some("[ ] 0/4".to_string())
304 );
305 assert_eq!(
306 format.progress_status(1, 4, ""),
307 Some("[===> ] 1/4".to_string())
308 );
309 assert_eq!(
310 format.progress_status(2, 4, ""),
311 Some("[========> ] 2/4".to_string())
312 );
313 assert_eq!(
314 format.progress_status(3, 4, ""),
315 Some("[=============> ] 3/4".to_string())
316 );
317 assert_eq!(
318 format.progress_status(4, 4, ""),
319 Some("[===================] 4/4".to_string())
320 );
321
322 assert_eq!(
323 format.progress_status(3999, 4000, ""),
324 Some("[===========> ] 3999/4000".to_string())
325 );
326 assert_eq!(
327 format.progress_status(4000, 4000, ""),
328 Some("[=============] 4000/4000".to_string())
329 );
330
331 assert_eq!(
332 format.progress_status(3, 4, ": short message"),
333 Some("[=============> ] 3/4: short message".to_string())
334 );
335 assert_eq!(
336 format.progress_status(3, 4, ": msg thats just fit"),
337 Some("[=============> ] 3/4: msg thats just fit".to_string())
338 );
339 assert_eq!(
340 format.progress_status(3, 4, ": msg that's just fit"),
341 Some("[=============> ] 3/4: msg that's just...".to_string())
342 );
343
344 // combining diacritics have width zero and thus can fit max_width.
345 let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠";
346 assert_eq!(
347 format.progress_status(3, 4, zalgo_msg),
348 Some("[=============> ] 3/4".to_string() + zalgo_msg)
349 );
350
351 // some non-ASCII ellipsize test
352 assert_eq!(
353 format.progress_status(3, 4, "_123456789123456e\u{301}\u{301}8\u{301}90a"),
354 Some("[=============> ] 3/4_123456789123456e\u{301}\u{301}...".to_string())
355 );
356 assert_eq!(
357 format.progress_status(3, 4, ":每個漢字佔據了兩個字元"),
358 Some("[=============> ] 3/4:每個漢字佔據了...".to_string())
359 );
360 }
361
362 #[test]
363 fn test_progress_status_percentage() {
364 let format = Format {
365 style: ProgressStyle::Percentage,
366 max_print: 40,
367 max_width: 60,
368 };
369 assert_eq!(
370 format.progress_status(0, 77, ""),
371 Some("[ ] 0.00%".to_string())
372 );
373 assert_eq!(
374 format.progress_status(1, 77, ""),
375 Some("[ ] 1.30%".to_string())
376 );
377 assert_eq!(
378 format.progress_status(76, 77, ""),
379 Some("[=============> ] 98.70%".to_string())
380 );
381 assert_eq!(
382 format.progress_status(77, 77, ""),
383 Some("[===============] 100.00%".to_string())
384 );
385 }
386
387 #[test]
388 fn test_progress_status_too_short() {
389 let format = Format {
390 style: ProgressStyle::Percentage,
391 max_print: 25,
392 max_width: 25,
393 };
394 assert_eq!(
395 format.progress_status(1, 1, ""),
396 Some("[] 100.00%".to_string())
397 );
398
399 let format = Format {
400 style: ProgressStyle::Percentage,
401 max_print: 24,
402 max_width: 24,
403 };
404 assert_eq!(
405 format.progress_status(1, 1, ""),
406 None
407 );
408 }