1 | use std::collections::HashMap; |
2 | use std::fmt::{self, Write}; |
3 | use std::mem; |
4 | #[cfg (not(target_arch = "wasm32" ))] |
5 | use std::time::Instant; |
6 | |
7 | use console::{measure_text_width, Style}; |
8 | #[cfg (target_arch = "wasm32" )] |
9 | use instant::Instant; |
10 | #[cfg (feature = "unicode-segmentation" )] |
11 | use unicode_segmentation::UnicodeSegmentation; |
12 | |
13 | use crate::format::{ |
14 | BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration, |
15 | HumanFloatCount, |
16 | }; |
17 | use crate::state::{ProgressState, TabExpandedString, DEFAULT_TAB_WIDTH}; |
18 | |
19 | #[derive (Clone)] |
20 | pub struct ProgressStyle { |
21 | tick_strings: Vec<Box<str>>, |
22 | progress_chars: Vec<Box<str>>, |
23 | template: Template, |
24 | // how unicode-big each char in progress_chars is |
25 | char_width: usize, |
26 | tab_width: usize, |
27 | pub(crate) format_map: HashMap<&'static str, Box<dyn ProgressTracker>>, |
28 | } |
29 | |
30 | #[cfg (feature = "unicode-segmentation" )] |
31 | fn segment(s: &str) -> Vec<Box<str>> { |
32 | UnicodeSegmentation::graphemes(s, true) |
33 | .map(|s| s.into()) |
34 | .collect() |
35 | } |
36 | |
37 | #[cfg (not(feature = "unicode-segmentation" ))] |
38 | fn segment(s: &str) -> Vec<Box<str>> { |
39 | s.chars().map(|x: char| x.to_string().into()).collect() |
40 | } |
41 | |
42 | #[cfg (feature = "unicode-width" )] |
43 | fn measure(s: &str) -> usize { |
44 | unicode_width::UnicodeWidthStr::width(self:s) |
45 | } |
46 | |
47 | #[cfg (not(feature = "unicode-width" ))] |
48 | fn measure(s: &str) -> usize { |
49 | s.chars().count() |
50 | } |
51 | |
52 | /// finds the unicode-aware width of the passed grapheme cluters |
53 | /// panics on an empty parameter, or if the characters are not equal-width |
54 | fn width(c: &[Box<str>]) -> usize { |
55 | cOption.iter() |
56 | .map(|s| measure(s.as_ref())) |
57 | .fold(init:None, |acc: Option, new: usize| { |
58 | match acc { |
59 | None => return Some(new), |
60 | Some(old: usize) => assert_eq!(old, new, "got passed un-equal width progress characters" ), |
61 | } |
62 | acc |
63 | }) |
64 | .unwrap() |
65 | } |
66 | |
67 | impl ProgressStyle { |
68 | /// Returns the default progress bar style for bars |
69 | pub fn default_bar() -> Self { |
70 | Self::new(Template::from_str("{wide_bar} {pos}/{len}" ).unwrap()) |
71 | } |
72 | |
73 | /// Returns the default progress bar style for spinners |
74 | pub fn default_spinner() -> Self { |
75 | Self::new(Template::from_str("{spinner} {msg}" ).unwrap()) |
76 | } |
77 | |
78 | /// Sets the template string for the progress bar |
79 | /// |
80 | /// Review the [list of template keys](../index.html#templates) for more information. |
81 | pub fn with_template(template: &str) -> Result<Self, TemplateError> { |
82 | Ok(Self::new(Template::from_str(template)?)) |
83 | } |
84 | |
85 | pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) { |
86 | self.tab_width = new_tab_width; |
87 | self.template.set_tab_width(new_tab_width); |
88 | } |
89 | |
90 | fn new(template: Template) -> Self { |
91 | let progress_chars = segment("█░" ); |
92 | let char_width = width(&progress_chars); |
93 | Self { |
94 | tick_strings: "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈ " |
95 | .chars() |
96 | .map(|c| c.to_string().into()) |
97 | .collect(), |
98 | progress_chars, |
99 | char_width, |
100 | template, |
101 | format_map: HashMap::default(), |
102 | tab_width: DEFAULT_TAB_WIDTH, |
103 | } |
104 | } |
105 | |
106 | /// Sets the tick character sequence for spinners |
107 | /// |
108 | /// Note that the last character is used as the [final tick string][Self::get_final_tick_str()]. |
109 | /// At least two characters are required to provide a non-final and final state. |
110 | pub fn tick_chars(mut self, s: &str) -> Self { |
111 | self.tick_strings = s.chars().map(|c| c.to_string().into()).collect(); |
112 | // Format bar will panic with some potentially confusing message, better to panic here |
113 | // with a message explicitly informing of the problem |
114 | assert!( |
115 | self.tick_strings.len() >= 2, |
116 | "at least 2 tick chars required" |
117 | ); |
118 | self |
119 | } |
120 | |
121 | /// Sets the tick string sequence for spinners |
122 | /// |
123 | /// Note that the last string is used as the [final tick string][Self::get_final_tick_str()]. |
124 | /// At least two strings are required to provide a non-final and final state. |
125 | pub fn tick_strings(mut self, s: &[&str]) -> Self { |
126 | self.tick_strings = s.iter().map(|s| s.to_string().into()).collect(); |
127 | // Format bar will panic with some potentially confusing message, better to panic here |
128 | // with a message explicitly informing of the problem |
129 | assert!( |
130 | self.progress_chars.len() >= 2, |
131 | "at least 2 tick strings required" |
132 | ); |
133 | self |
134 | } |
135 | |
136 | /// Sets the progress characters `(filled, current, to do)` |
137 | /// |
138 | /// You can pass more than three for a more detailed display. |
139 | /// All passed grapheme clusters need to be of equal width. |
140 | pub fn progress_chars(mut self, s: &str) -> Self { |
141 | self.progress_chars = segment(s); |
142 | // Format bar will panic with some potentially confusing message, better to panic here |
143 | // with a message explicitly informing of the problem |
144 | assert!( |
145 | self.progress_chars.len() >= 2, |
146 | "at least 2 progress chars required" |
147 | ); |
148 | self.char_width = width(&self.progress_chars); |
149 | self |
150 | } |
151 | |
152 | /// Adds a custom key that owns a [`ProgressTracker`] to the template |
153 | pub fn with_key<S: ProgressTracker + 'static>(mut self, key: &'static str, f: S) -> Self { |
154 | self.format_map.insert(key, Box::new(f)); |
155 | self |
156 | } |
157 | |
158 | /// Sets the template string for the progress bar |
159 | /// |
160 | /// Review the [list of template keys](../index.html#templates) for more information. |
161 | pub fn template(mut self, s: &str) -> Result<Self, TemplateError> { |
162 | self.template = Template::from_str(s)?; |
163 | Ok(self) |
164 | } |
165 | |
166 | fn current_tick_str(&self, state: &ProgressState) -> &str { |
167 | match state.is_finished() { |
168 | true => self.get_final_tick_str(), |
169 | false => self.get_tick_str(state.tick), |
170 | } |
171 | } |
172 | |
173 | /// Returns the tick string for a given number |
174 | pub fn get_tick_str(&self, idx: u64) -> &str { |
175 | &self.tick_strings[(idx as usize) % (self.tick_strings.len() - 1)] |
176 | } |
177 | |
178 | /// Returns the tick string for the finished state |
179 | pub fn get_final_tick_str(&self) -> &str { |
180 | &self.tick_strings[self.tick_strings.len() - 1] |
181 | } |
182 | |
183 | fn format_bar(&self, fract: f32, width: usize, alt_style: Option<&Style>) -> BarDisplay<'_> { |
184 | // The number of clusters from progress_chars to write (rounding down). |
185 | let width = width / self.char_width; |
186 | // The number of full clusters (including a fractional component for a partially-full one). |
187 | let fill = fract * width as f32; |
188 | // The number of entirely full clusters (by truncating `fill`). |
189 | let entirely_filled = fill as usize; |
190 | // 1 if the bar is not entirely empty or full (meaning we need to draw the "current" |
191 | // character between the filled and "to do" segment), 0 otherwise. |
192 | let head = usize::from(fill > 0.0 && entirely_filled < width); |
193 | |
194 | let cur = if head == 1 { |
195 | // Number of fine-grained progress entries in progress_chars. |
196 | let n = self.progress_chars.len().saturating_sub(2); |
197 | let cur_char = if n <= 1 { |
198 | // No fine-grained entries. 1 is the single "current" entry if we have one, the "to |
199 | // do" entry if not. |
200 | 1 |
201 | } else { |
202 | // Pick a fine-grained entry, ranging from the last one (n) if the fractional part |
203 | // of fill is 0 to the first one (1) if the fractional part of fill is almost 1. |
204 | n.saturating_sub((fill.fract() * n as f32) as usize) |
205 | }; |
206 | Some(cur_char) |
207 | } else { |
208 | None |
209 | }; |
210 | |
211 | // Number of entirely empty clusters needed to fill the bar up to `width`. |
212 | let bg = width.saturating_sub(entirely_filled).saturating_sub(head); |
213 | let rest = RepeatedStringDisplay { |
214 | str: &self.progress_chars[self.progress_chars.len() - 1], |
215 | num: bg, |
216 | }; |
217 | |
218 | BarDisplay { |
219 | chars: &self.progress_chars, |
220 | filled: entirely_filled, |
221 | cur, |
222 | rest: alt_style.unwrap_or(&Style::new()).apply_to(rest), |
223 | } |
224 | } |
225 | |
226 | pub(crate) fn format_state( |
227 | &self, |
228 | state: &ProgressState, |
229 | lines: &mut Vec<String>, |
230 | target_width: u16, |
231 | ) { |
232 | let mut cur = String::new(); |
233 | let mut buf = String::new(); |
234 | let mut wide = None; |
235 | |
236 | let pos = state.pos(); |
237 | let len = state.len().unwrap_or(pos); |
238 | for part in &self.template.parts { |
239 | match part { |
240 | TemplatePart::Placeholder { |
241 | key, |
242 | align, |
243 | width, |
244 | truncate, |
245 | style, |
246 | alt_style, |
247 | } => { |
248 | buf.clear(); |
249 | if let Some(tracker) = self.format_map.get(key.as_str()) { |
250 | tracker.write(state, &mut TabRewriter(&mut buf, self.tab_width)); |
251 | } else { |
252 | match key.as_str() { |
253 | "wide_bar" => { |
254 | wide = Some(WideElement::Bar { alt_style }); |
255 | buf.push(' \x00' ); |
256 | } |
257 | "bar" => buf |
258 | .write_fmt(format_args!( |
259 | " {}" , |
260 | self.format_bar( |
261 | state.fraction(), |
262 | width.unwrap_or(20) as usize, |
263 | alt_style.as_ref(), |
264 | ) |
265 | )) |
266 | .unwrap(), |
267 | "spinner" => buf.push_str(self.current_tick_str(state)), |
268 | "wide_msg" => { |
269 | wide = Some(WideElement::Message { align }); |
270 | buf.push(' \x00' ); |
271 | } |
272 | "msg" => buf.push_str(state.message.expanded()), |
273 | "prefix" => buf.push_str(state.prefix.expanded()), |
274 | "pos" => buf.write_fmt(format_args!(" {pos}" )).unwrap(), |
275 | "human_pos" => { |
276 | buf.write_fmt(format_args!(" {}" , HumanCount(pos))).unwrap(); |
277 | } |
278 | "len" => buf.write_fmt(format_args!(" {len}" )).unwrap(), |
279 | "human_len" => { |
280 | buf.write_fmt(format_args!(" {}" , HumanCount(len))).unwrap(); |
281 | } |
282 | "percent" => buf |
283 | .write_fmt(format_args!(" {:.*}" , 0, state.fraction() * 100f32)) |
284 | .unwrap(), |
285 | "bytes" => buf.write_fmt(format_args!(" {}" , HumanBytes(pos))).unwrap(), |
286 | "total_bytes" => { |
287 | buf.write_fmt(format_args!(" {}" , HumanBytes(len))).unwrap(); |
288 | } |
289 | "decimal_bytes" => buf |
290 | .write_fmt(format_args!(" {}" , DecimalBytes(pos))) |
291 | .unwrap(), |
292 | "decimal_total_bytes" => buf |
293 | .write_fmt(format_args!(" {}" , DecimalBytes(len))) |
294 | .unwrap(), |
295 | "binary_bytes" => { |
296 | buf.write_fmt(format_args!(" {}" , BinaryBytes(pos))).unwrap(); |
297 | } |
298 | "binary_total_bytes" => { |
299 | buf.write_fmt(format_args!(" {}" , BinaryBytes(len))).unwrap(); |
300 | } |
301 | "elapsed_precise" => buf |
302 | .write_fmt(format_args!(" {}" , FormattedDuration(state.elapsed()))) |
303 | .unwrap(), |
304 | "elapsed" => buf |
305 | .write_fmt(format_args!(" {:#}" , HumanDuration(state.elapsed()))) |
306 | .unwrap(), |
307 | "per_sec" => buf |
308 | .write_fmt(format_args!(" {}/s" , HumanFloatCount(state.per_sec()))) |
309 | .unwrap(), |
310 | "bytes_per_sec" => buf |
311 | .write_fmt(format_args!(" {}/s" , HumanBytes(state.per_sec() as u64))) |
312 | .unwrap(), |
313 | "binary_bytes_per_sec" => buf |
314 | .write_fmt(format_args!( |
315 | " {}/s" , |
316 | BinaryBytes(state.per_sec() as u64) |
317 | )) |
318 | .unwrap(), |
319 | "eta_precise" => buf |
320 | .write_fmt(format_args!(" {}" , FormattedDuration(state.eta()))) |
321 | .unwrap(), |
322 | "eta" => buf |
323 | .write_fmt(format_args!(" {:#}" , HumanDuration(state.eta()))) |
324 | .unwrap(), |
325 | "duration_precise" => buf |
326 | .write_fmt(format_args!(" {}" , FormattedDuration(state.duration()))) |
327 | .unwrap(), |
328 | "duration" => buf |
329 | .write_fmt(format_args!(" {:#}" , HumanDuration(state.duration()))) |
330 | .unwrap(), |
331 | _ => (), |
332 | } |
333 | }; |
334 | |
335 | match width { |
336 | Some(width) => { |
337 | let padded = PaddedStringDisplay { |
338 | str: &buf, |
339 | width: *width as usize, |
340 | align: *align, |
341 | truncate: *truncate, |
342 | }; |
343 | match style { |
344 | Some(s) => cur |
345 | .write_fmt(format_args!(" {}" , s.apply_to(padded))) |
346 | .unwrap(), |
347 | None => cur.write_fmt(format_args!(" {padded}" )).unwrap(), |
348 | } |
349 | } |
350 | None => match style { |
351 | Some(s) => cur.write_fmt(format_args!(" {}" , s.apply_to(&buf))).unwrap(), |
352 | None => cur.push_str(&buf), |
353 | }, |
354 | } |
355 | } |
356 | TemplatePart::Literal(s) => cur.push_str(s.expanded()), |
357 | TemplatePart::NewLine => { |
358 | self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide); |
359 | } |
360 | } |
361 | } |
362 | |
363 | if !cur.is_empty() { |
364 | self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide); |
365 | } |
366 | } |
367 | |
368 | fn push_line( |
369 | &self, |
370 | lines: &mut Vec<String>, |
371 | cur: &mut String, |
372 | state: &ProgressState, |
373 | buf: &mut String, |
374 | target_width: u16, |
375 | wide: &Option<WideElement>, |
376 | ) { |
377 | let expanded = match wide { |
378 | Some(inner) => inner.expand(mem::take(cur), self, state, buf, target_width), |
379 | None => mem::take(cur), |
380 | }; |
381 | |
382 | // If there are newlines, we need to split them up |
383 | // and add the lines separately so that they're counted |
384 | // correctly on re-render. |
385 | for (i, line) in expanded.split(' \n' ).enumerate() { |
386 | // No newlines found in this case |
387 | if i == 0 && line.len() == expanded.len() { |
388 | lines.push(expanded); |
389 | break; |
390 | } |
391 | |
392 | lines.push(line.to_string()); |
393 | } |
394 | } |
395 | } |
396 | |
397 | struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize); |
398 | |
399 | impl Write for TabRewriter<'_> { |
400 | fn write_str(&mut self, s: &str) -> fmt::Result { |
401 | self.0 |
402 | .write_str(s.replace(from:' \t' , &" " .repeat(self.1)).as_str()) |
403 | } |
404 | } |
405 | |
406 | #[derive (Clone, Copy)] |
407 | enum WideElement<'a> { |
408 | Bar { alt_style: &'a Option<Style> }, |
409 | Message { align: &'a Alignment }, |
410 | } |
411 | |
412 | impl<'a> WideElement<'a> { |
413 | fn expand( |
414 | self, |
415 | cur: String, |
416 | style: &ProgressStyle, |
417 | state: &ProgressState, |
418 | buf: &mut String, |
419 | width: u16, |
420 | ) -> String { |
421 | let left = (width as usize).saturating_sub(measure_text_width(&cur.replace(' \x00' , "" ))); |
422 | match self { |
423 | Self::Bar { alt_style } => cur.replace( |
424 | ' \x00' , |
425 | &format!( |
426 | " {}" , |
427 | style.format_bar(state.fraction(), left, alt_style.as_ref()) |
428 | ), |
429 | ), |
430 | WideElement::Message { align } => { |
431 | buf.clear(); |
432 | buf.write_fmt(format_args!( |
433 | " {}" , |
434 | PaddedStringDisplay { |
435 | str: state.message.expanded(), |
436 | width: left, |
437 | align: *align, |
438 | truncate: true, |
439 | } |
440 | )) |
441 | .unwrap(); |
442 | |
443 | let trimmed = match cur.as_bytes().last() == Some(&b' \x00' ) { |
444 | true => buf.trim_end(), |
445 | false => buf, |
446 | }; |
447 | |
448 | cur.replace(' \x00' , trimmed) |
449 | } |
450 | } |
451 | } |
452 | } |
453 | |
454 | #[derive (Clone, Debug)] |
455 | struct Template { |
456 | parts: Vec<TemplatePart>, |
457 | } |
458 | |
459 | impl Template { |
460 | fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> { |
461 | use State::*; |
462 | let (mut state, mut parts, mut buf) = (Literal, vec![], String::new()); |
463 | for c in s.chars() { |
464 | let new = match (state, c) { |
465 | (Literal, '{' ) => (MaybeOpen, None), |
466 | (Literal, ' \n' ) => { |
467 | if !buf.is_empty() { |
468 | parts.push(TemplatePart::Literal(TabExpandedString::new( |
469 | mem::take(&mut buf).into(), |
470 | tab_width, |
471 | ))); |
472 | } |
473 | parts.push(TemplatePart::NewLine); |
474 | (Literal, None) |
475 | } |
476 | (Literal, '}' ) => (DoubleClose, Some('}' )), |
477 | (Literal, c) => (Literal, Some(c)), |
478 | (DoubleClose, '}' ) => (Literal, None), |
479 | (MaybeOpen, '{' ) => (Literal, Some('{' )), |
480 | (MaybeOpen | Key, c) if c.is_ascii_whitespace() => { |
481 | // If we find whitespace where the variable key is supposed to go, |
482 | // backtrack and act as if this was a literal. |
483 | buf.push(c); |
484 | let mut new = String::from("{" ); |
485 | new.push_str(&buf); |
486 | buf.clear(); |
487 | parts.push(TemplatePart::Literal(TabExpandedString::new( |
488 | new.into(), |
489 | tab_width, |
490 | ))); |
491 | (Literal, None) |
492 | } |
493 | (MaybeOpen, c) if c != '}' && c != ':' => (Key, Some(c)), |
494 | (Key, c) if c != '}' && c != ':' => (Key, Some(c)), |
495 | (Key, ':' ) => (Align, None), |
496 | (Key, '}' ) => (Literal, None), |
497 | (Key, '!' ) if !buf.is_empty() => { |
498 | parts.push(TemplatePart::Placeholder { |
499 | key: mem::take(&mut buf), |
500 | align: Alignment::Left, |
501 | width: None, |
502 | truncate: true, |
503 | style: None, |
504 | alt_style: None, |
505 | }); |
506 | (Width, None) |
507 | } |
508 | (Align, c) if c == '<' || c == '^' || c == '>' => { |
509 | if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() { |
510 | match c { |
511 | '<' => *align = Alignment::Left, |
512 | '^' => *align = Alignment::Center, |
513 | '>' => *align = Alignment::Right, |
514 | _ => (), |
515 | } |
516 | } |
517 | |
518 | (Width, None) |
519 | } |
520 | (Align, c @ '0' ..='9' ) => (Width, Some(c)), |
521 | (Align | Width, '!' ) => { |
522 | if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() { |
523 | *truncate = true; |
524 | } |
525 | (Width, None) |
526 | } |
527 | (Align, '.' ) => (FirstStyle, None), |
528 | (Align, '}' ) => (Literal, None), |
529 | (Width, c @ '0' ..='9' ) => (Width, Some(c)), |
530 | (Width, '.' ) => (FirstStyle, None), |
531 | (Width, '}' ) => (Literal, None), |
532 | (FirstStyle, '/' ) => (AltStyle, None), |
533 | (FirstStyle, '}' ) => (Literal, None), |
534 | (FirstStyle, c) => (FirstStyle, Some(c)), |
535 | (AltStyle, '}' ) => (Literal, None), |
536 | (AltStyle, c) => (AltStyle, Some(c)), |
537 | (st, c) => return Err(TemplateError { next: c, state: st }), |
538 | }; |
539 | |
540 | match (state, new.0) { |
541 | (MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal( |
542 | TabExpandedString::new(mem::take(&mut buf).into(), tab_width), |
543 | )), |
544 | (Key, Align | Literal) if !buf.is_empty() => { |
545 | parts.push(TemplatePart::Placeholder { |
546 | key: mem::take(&mut buf), |
547 | align: Alignment::Left, |
548 | width: None, |
549 | truncate: false, |
550 | style: None, |
551 | alt_style: None, |
552 | }); |
553 | } |
554 | (Width, FirstStyle | Literal) if !buf.is_empty() => { |
555 | if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() { |
556 | *width = Some(buf.parse().unwrap()); |
557 | buf.clear(); |
558 | } |
559 | } |
560 | (FirstStyle, AltStyle | Literal) if !buf.is_empty() => { |
561 | if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() { |
562 | *style = Some(Style::from_dotted_str(&buf)); |
563 | buf.clear(); |
564 | } |
565 | } |
566 | (AltStyle, Literal) if !buf.is_empty() => { |
567 | if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() { |
568 | *alt_style = Some(Style::from_dotted_str(&buf)); |
569 | buf.clear(); |
570 | } |
571 | } |
572 | (_, _) => (), |
573 | } |
574 | |
575 | state = new.0; |
576 | if let Some(c) = new.1 { |
577 | buf.push(c); |
578 | } |
579 | } |
580 | |
581 | if matches!(state, Literal | DoubleClose) && !buf.is_empty() { |
582 | parts.push(TemplatePart::Literal(TabExpandedString::new( |
583 | buf.into(), |
584 | tab_width, |
585 | ))); |
586 | } |
587 | |
588 | Ok(Self { parts }) |
589 | } |
590 | |
591 | fn from_str(s: &str) -> Result<Self, TemplateError> { |
592 | Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH) |
593 | } |
594 | |
595 | fn set_tab_width(&mut self, new_tab_width: usize) { |
596 | for part in &mut self.parts { |
597 | if let TemplatePart::Literal(s) = part { |
598 | s.set_tab_width(new_tab_width); |
599 | } |
600 | } |
601 | } |
602 | } |
603 | |
604 | #[derive (Debug)] |
605 | pub struct TemplateError { |
606 | state: State, |
607 | next: char, |
608 | } |
609 | |
610 | impl fmt::Display for TemplateError { |
611 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
612 | write!( |
613 | f, |
614 | "TemplateError: unexpected character {:?} in state {:?}" , |
615 | self.next, self.state |
616 | ) |
617 | } |
618 | } |
619 | |
620 | impl std::error::Error for TemplateError {} |
621 | |
622 | #[derive (Clone, Debug, PartialEq, Eq)] |
623 | enum TemplatePart { |
624 | Literal(TabExpandedString), |
625 | Placeholder { |
626 | key: String, |
627 | align: Alignment, |
628 | width: Option<u16>, |
629 | truncate: bool, |
630 | style: Option<Style>, |
631 | alt_style: Option<Style>, |
632 | }, |
633 | NewLine, |
634 | } |
635 | |
636 | #[derive (Copy, Clone, Debug, PartialEq, Eq)] |
637 | enum State { |
638 | Literal, |
639 | MaybeOpen, |
640 | DoubleClose, |
641 | Key, |
642 | Align, |
643 | Width, |
644 | FirstStyle, |
645 | AltStyle, |
646 | } |
647 | |
648 | struct BarDisplay<'a> { |
649 | chars: &'a [Box<str>], |
650 | filled: usize, |
651 | cur: Option<usize>, |
652 | rest: console::StyledObject<RepeatedStringDisplay<'a>>, |
653 | } |
654 | |
655 | impl<'a> fmt::Display for BarDisplay<'a> { |
656 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
657 | for _ in 0..self.filled { |
658 | f.write_str(&self.chars[0])?; |
659 | } |
660 | if let Some(cur: usize) = self.cur { |
661 | f.write_str(&self.chars[cur])?; |
662 | } |
663 | self.rest.fmt(f) |
664 | } |
665 | } |
666 | |
667 | struct RepeatedStringDisplay<'a> { |
668 | str: &'a str, |
669 | num: usize, |
670 | } |
671 | |
672 | impl<'a> fmt::Display for RepeatedStringDisplay<'a> { |
673 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
674 | for _ in 0..self.num { |
675 | f.write_str(self.str)?; |
676 | } |
677 | Ok(()) |
678 | } |
679 | } |
680 | |
681 | struct PaddedStringDisplay<'a> { |
682 | str: &'a str, |
683 | width: usize, |
684 | align: Alignment, |
685 | truncate: bool, |
686 | } |
687 | |
688 | impl<'a> fmt::Display for PaddedStringDisplay<'a> { |
689 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
690 | let cols = measure_text_width(self.str); |
691 | let excess = cols.saturating_sub(self.width); |
692 | if excess > 0 && !self.truncate { |
693 | return f.write_str(self.str); |
694 | } else if excess > 0 { |
695 | let (start, end) = match self.align { |
696 | Alignment::Left => (0, self.str.len() - excess), |
697 | Alignment::Right => (excess, self.str.len()), |
698 | Alignment::Center => ( |
699 | excess / 2, |
700 | self.str.len() - excess.saturating_sub(excess / 2), |
701 | ), |
702 | }; |
703 | |
704 | return f.write_str(self.str.get(start..end).unwrap_or(self.str)); |
705 | } |
706 | |
707 | let diff = self.width.saturating_sub(cols); |
708 | let (left_pad, right_pad) = match self.align { |
709 | Alignment::Left => (0, diff), |
710 | Alignment::Right => (diff, 0), |
711 | Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)), |
712 | }; |
713 | |
714 | for _ in 0..left_pad { |
715 | f.write_char(' ' )?; |
716 | } |
717 | f.write_str(self.str)?; |
718 | for _ in 0..right_pad { |
719 | f.write_char(' ' )?; |
720 | } |
721 | Ok(()) |
722 | } |
723 | } |
724 | |
725 | #[derive (PartialEq, Eq, Debug, Copy, Clone)] |
726 | enum Alignment { |
727 | Left, |
728 | Center, |
729 | Right, |
730 | } |
731 | |
732 | /// Trait for defining stateful or stateless formatters |
733 | pub trait ProgressTracker: Send + Sync { |
734 | /// Creates a new instance of the progress tracker |
735 | fn clone_box(&self) -> Box<dyn ProgressTracker>; |
736 | /// Notifies the progress tracker of a tick event |
737 | fn tick(&mut self, state: &ProgressState, now: Instant); |
738 | /// Notifies the progress tracker of a reset event |
739 | fn reset(&mut self, state: &ProgressState, now: Instant); |
740 | /// Provides access to the progress bar display buffer for custom messages |
741 | fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write); |
742 | } |
743 | |
744 | impl Clone for Box<dyn ProgressTracker> { |
745 | fn clone(&self) -> Self { |
746 | self.clone_box() |
747 | } |
748 | } |
749 | |
750 | impl<F> ProgressTracker for F |
751 | where |
752 | F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Sync + Clone + 'static, |
753 | { |
754 | fn clone_box(&self) -> Box<dyn ProgressTracker> { |
755 | Box::new(self.clone()) |
756 | } |
757 | |
758 | fn tick(&mut self, _: &ProgressState, _: Instant) {} |
759 | |
760 | fn reset(&mut self, _: &ProgressState, _: Instant) {} |
761 | |
762 | fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) { |
763 | (self)(state, w); |
764 | } |
765 | } |
766 | |
767 | #[cfg (test)] |
768 | mod tests { |
769 | use std::sync::Arc; |
770 | |
771 | use super::*; |
772 | use crate::state::{AtomicPosition, ProgressState}; |
773 | use std::sync::Mutex; |
774 | |
775 | #[test ] |
776 | fn test_stateful_tracker() { |
777 | #[derive (Debug, Clone)] |
778 | struct TestTracker(Arc<Mutex<String>>); |
779 | |
780 | impl ProgressTracker for TestTracker { |
781 | fn clone_box(&self) -> Box<dyn ProgressTracker> { |
782 | Box::new(self.clone()) |
783 | } |
784 | |
785 | fn tick(&mut self, state: &ProgressState, _: Instant) { |
786 | let mut m = self.0.lock().unwrap(); |
787 | m.clear(); |
788 | m.push_str(format!(" {} {}" , state.len().unwrap(), state.pos()).as_str()); |
789 | } |
790 | |
791 | fn reset(&mut self, _state: &ProgressState, _: Instant) { |
792 | let mut m = self.0.lock().unwrap(); |
793 | m.clear(); |
794 | } |
795 | |
796 | fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) { |
797 | w.write_str(self.0.lock().unwrap().as_str()).unwrap(); |
798 | } |
799 | } |
800 | |
801 | use crate::ProgressBar; |
802 | |
803 | let pb = ProgressBar::new(1); |
804 | pb.set_style( |
805 | ProgressStyle::with_template("{{ {foo} }}" ) |
806 | .unwrap() |
807 | .with_key("foo" , TestTracker(Arc::new(Mutex::new(String::default())))) |
808 | .progress_chars("#>-" ), |
809 | ); |
810 | |
811 | let mut buf = Vec::new(); |
812 | let style = pb.clone().style(); |
813 | |
814 | style.format_state(&pb.state().state, &mut buf, 16); |
815 | assert_eq!(&buf[0], "{ }" ); |
816 | buf.clear(); |
817 | pb.inc(1); |
818 | style.format_state(&pb.state().state, &mut buf, 16); |
819 | assert_eq!(&buf[0], "{ 1 1 }" ); |
820 | pb.reset(); |
821 | buf.clear(); |
822 | style.format_state(&pb.state().state, &mut buf, 16); |
823 | assert_eq!(&buf[0], "{ }" ); |
824 | pb.finish_and_clear(); |
825 | } |
826 | |
827 | use crate::state::TabExpandedString; |
828 | |
829 | #[test ] |
830 | fn test_expand_template() { |
831 | const WIDTH: u16 = 80; |
832 | let pos = Arc::new(AtomicPosition::new()); |
833 | let state = ProgressState::new(Some(10), pos); |
834 | let mut buf = Vec::new(); |
835 | |
836 | let mut style = ProgressStyle::default_bar(); |
837 | style.format_map.insert( |
838 | "foo" , |
839 | Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO" ).unwrap()), |
840 | ); |
841 | style.format_map.insert( |
842 | "bar" , |
843 | Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR" ).unwrap()), |
844 | ); |
845 | |
846 | style.template = Template::from_str("{{ {foo} {bar} }}" ).unwrap(); |
847 | style.format_state(&state, &mut buf, WIDTH); |
848 | assert_eq!(&buf[0], "{ FOO BAR }" ); |
849 | |
850 | buf.clear(); |
851 | style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"# ).unwrap(); |
852 | style.format_state(&state, &mut buf, WIDTH); |
853 | assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"# ); |
854 | } |
855 | |
856 | #[test ] |
857 | fn test_expand_template_flags() { |
858 | use console::set_colors_enabled; |
859 | set_colors_enabled(true); |
860 | |
861 | const WIDTH: u16 = 80; |
862 | let pos = Arc::new(AtomicPosition::new()); |
863 | let state = ProgressState::new(Some(10), pos); |
864 | let mut buf = Vec::new(); |
865 | |
866 | let mut style = ProgressStyle::default_bar(); |
867 | style.format_map.insert( |
868 | "foo" , |
869 | Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX" ).unwrap()), |
870 | ); |
871 | |
872 | style.template = Template::from_str("{foo:5}" ).unwrap(); |
873 | style.format_state(&state, &mut buf, WIDTH); |
874 | assert_eq!(&buf[0], "XXX " ); |
875 | |
876 | buf.clear(); |
877 | style.template = Template::from_str("{foo:.red.on_blue}" ).unwrap(); |
878 | style.format_state(&state, &mut buf, WIDTH); |
879 | assert_eq!(&buf[0], " \u{1b}[31m \u{1b}[44mXXX \u{1b}[0m" ); |
880 | |
881 | buf.clear(); |
882 | style.template = Template::from_str("{foo:^5.red.on_blue}" ).unwrap(); |
883 | style.format_state(&state, &mut buf, WIDTH); |
884 | assert_eq!(&buf[0], " \u{1b}[31m \u{1b}[44m XXX \u{1b}[0m" ); |
885 | |
886 | buf.clear(); |
887 | style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}" ).unwrap(); |
888 | style.format_state(&state, &mut buf, WIDTH); |
889 | assert_eq!(&buf[0], " \u{1b}[31m \u{1b}[44m XXX \u{1b}[0m" ); |
890 | } |
891 | |
892 | #[test ] |
893 | fn align_truncation() { |
894 | const WIDTH: u16 = 10; |
895 | let pos = Arc::new(AtomicPosition::new()); |
896 | let mut state = ProgressState::new(Some(10), pos); |
897 | let mut buf = Vec::new(); |
898 | |
899 | let style = ProgressStyle::with_template("{wide_msg}" ).unwrap(); |
900 | state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst" .into()); |
901 | style.format_state(&state, &mut buf, WIDTH); |
902 | assert_eq!(&buf[0], "abcdefghij" ); |
903 | |
904 | buf.clear(); |
905 | let style = ProgressStyle::with_template("{wide_msg:>}" ).unwrap(); |
906 | state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst" .into()); |
907 | style.format_state(&state, &mut buf, WIDTH); |
908 | assert_eq!(&buf[0], "klmnopqrst" ); |
909 | |
910 | buf.clear(); |
911 | let style = ProgressStyle::with_template("{wide_msg:^}" ).unwrap(); |
912 | state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst" .into()); |
913 | style.format_state(&state, &mut buf, WIDTH); |
914 | assert_eq!(&buf[0], "fghijklmno" ); |
915 | } |
916 | |
917 | #[test ] |
918 | fn wide_element_style() { |
919 | const CHARS: &str = "=>-" ; |
920 | const WIDTH: u16 = 8; |
921 | let pos = Arc::new(AtomicPosition::new()); |
922 | // half finished |
923 | pos.set(2); |
924 | let mut state = ProgressState::new(Some(4), pos); |
925 | let mut buf = Vec::new(); |
926 | |
927 | let style = ProgressStyle::with_template("{wide_bar}" ) |
928 | .unwrap() |
929 | .progress_chars(CHARS); |
930 | style.format_state(&state, &mut buf, WIDTH); |
931 | assert_eq!(&buf[0], "====>---" ); |
932 | |
933 | buf.clear(); |
934 | let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}" ) |
935 | .unwrap() |
936 | .progress_chars(CHARS); |
937 | style.format_state(&state, &mut buf, WIDTH); |
938 | assert_eq!( |
939 | &buf[0], |
940 | " \u{1b}[31m \u{1b}[44m====> \u{1b}[32m \u{1b}[46m--- \u{1b}[0m \u{1b}[0m" |
941 | ); |
942 | |
943 | buf.clear(); |
944 | let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}" ).unwrap(); |
945 | state.message = TabExpandedString::NoTabs("foobar" .into()); |
946 | style.format_state(&state, &mut buf, WIDTH); |
947 | assert_eq!(&buf[0], " \u{1b}[31m \u{1b}[44m foobar \u{1b}[0m" ); |
948 | } |
949 | |
950 | #[test ] |
951 | fn multiline_handling() { |
952 | const WIDTH: u16 = 80; |
953 | let pos = Arc::new(AtomicPosition::new()); |
954 | let mut state = ProgressState::new(Some(10), pos); |
955 | let mut buf = Vec::new(); |
956 | |
957 | let mut style = ProgressStyle::default_bar(); |
958 | state.message = TabExpandedString::new("foo \nbar \nbaz" .into(), 2); |
959 | style.template = Template::from_str("{msg}" ).unwrap(); |
960 | style.format_state(&state, &mut buf, WIDTH); |
961 | |
962 | assert_eq!(buf.len(), 3); |
963 | assert_eq!(&buf[0], "foo" ); |
964 | assert_eq!(&buf[1], "bar" ); |
965 | assert_eq!(&buf[2], "baz" ); |
966 | |
967 | buf.clear(); |
968 | style.template = Template::from_str("{wide_msg}" ).unwrap(); |
969 | style.format_state(&state, &mut buf, WIDTH); |
970 | |
971 | assert_eq!(buf.len(), 3); |
972 | assert_eq!(&buf[0], "foo" ); |
973 | assert_eq!(&buf[1], "bar" ); |
974 | assert_eq!(&buf[2], "baz" ); |
975 | |
976 | buf.clear(); |
977 | state.prefix = TabExpandedString::new("prefix \nprefix" .into(), 2); |
978 | style.template = Template::from_str("{prefix} {wide_msg}" ).unwrap(); |
979 | style.format_state(&state, &mut buf, WIDTH); |
980 | |
981 | assert_eq!(buf.len(), 4); |
982 | assert_eq!(&buf[0], "prefix" ); |
983 | assert_eq!(&buf[1], "prefix foo" ); |
984 | assert_eq!(&buf[2], "bar" ); |
985 | assert_eq!(&buf[3], "baz" ); |
986 | } |
987 | } |
988 | |