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(feature = "unicode-segmentation")] |
9 | use unicode_segmentation::UnicodeSegmentation; |
10 | #[cfg(target_arch = "wasm32")] |
11 | use web_time::Instant; |
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 |
56 | .map(|s| measure(s.as_ref())) |
57 | .fold(init:None, |acc: Option |
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 | "percent_precise"=> buf |
286 | .write_fmt(format_args!("{:.*} ", 3, state.fraction() * 100f32)) |
287 | .unwrap(), |
288 | "bytes"=> buf.write_fmt(format_args!( "{} ", HumanBytes(pos))).unwrap(), |
289 | "total_bytes"=> { |
290 | buf.write_fmt(format_args!("{} ", HumanBytes(len))).unwrap(); |
291 | } |
292 | "decimal_bytes"=> buf |
293 | .write_fmt(format_args!("{} ", DecimalBytes(pos))) |
294 | .unwrap(), |
295 | "decimal_total_bytes"=> buf |
296 | .write_fmt(format_args!("{} ", DecimalBytes(len))) |
297 | .unwrap(), |
298 | "binary_bytes"=> { |
299 | buf.write_fmt(format_args!("{} ", BinaryBytes(pos))).unwrap(); |
300 | } |
301 | "binary_total_bytes"=> { |
302 | buf.write_fmt(format_args!("{} ", BinaryBytes(len))).unwrap(); |
303 | } |
304 | "elapsed_precise"=> buf |
305 | .write_fmt(format_args!("{} ", FormattedDuration(state.elapsed()))) |
306 | .unwrap(), |
307 | "elapsed"=> buf |
308 | .write_fmt(format_args!("{:#} ", HumanDuration(state.elapsed()))) |
309 | .unwrap(), |
310 | "per_sec"=> buf |
311 | .write_fmt(format_args!("{} /s", HumanFloatCount(state.per_sec()))) |
312 | .unwrap(), |
313 | "bytes_per_sec"=> buf |
314 | .write_fmt(format_args!("{} /s", HumanBytes(state.per_sec() as u64))) |
315 | .unwrap(), |
316 | "decimal_bytes_per_sec"=> buf |
317 | .write_fmt(format_args!( |
318 | "{} /s", |
319 | DecimalBytes(state.per_sec() as u64) |
320 | )) |
321 | .unwrap(), |
322 | "binary_bytes_per_sec"=> buf |
323 | .write_fmt(format_args!( |
324 | "{} /s", |
325 | BinaryBytes(state.per_sec() as u64) |
326 | )) |
327 | .unwrap(), |
328 | "eta_precise"=> buf |
329 | .write_fmt(format_args!("{} ", FormattedDuration(state.eta()))) |
330 | .unwrap(), |
331 | "eta"=> buf |
332 | .write_fmt(format_args!("{:#} ", HumanDuration(state.eta()))) |
333 | .unwrap(), |
334 | "duration_precise"=> buf |
335 | .write_fmt(format_args!("{} ", FormattedDuration(state.duration()))) |
336 | .unwrap(), |
337 | "duration"=> buf |
338 | .write_fmt(format_args!("{:#} ", HumanDuration(state.duration()))) |
339 | .unwrap(), |
340 | _ => (), |
341 | } |
342 | }; |
343 | |
344 | match width { |
345 | Some(width) => { |
346 | let padded = PaddedStringDisplay { |
347 | str: &buf, |
348 | width: *width as usize, |
349 | align: *align, |
350 | truncate: *truncate, |
351 | }; |
352 | match style { |
353 | Some(s) => cur |
354 | .write_fmt(format_args!("{} ", s.apply_to(padded))) |
355 | .unwrap(), |
356 | None => cur.write_fmt(format_args!("{padded} ")).unwrap(), |
357 | } |
358 | } |
359 | None => match style { |
360 | Some(s) => cur.write_fmt(format_args!("{} ", s.apply_to(&buf))).unwrap(), |
361 | None => cur.push_str(&buf), |
362 | }, |
363 | } |
364 | } |
365 | TemplatePart::Literal(s) => cur.push_str(s.expanded()), |
366 | TemplatePart::NewLine => { |
367 | self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide); |
368 | } |
369 | } |
370 | } |
371 | |
372 | if !cur.is_empty() { |
373 | self.push_line(lines, &mut cur, state, &mut buf, target_width, &wide); |
374 | } |
375 | } |
376 | |
377 | fn push_line( |
378 | &self, |
379 | lines: &mut Vec<String>, |
380 | cur: &mut String, |
381 | state: &ProgressState, |
382 | buf: &mut String, |
383 | target_width: u16, |
384 | wide: &Option<WideElement>, |
385 | ) { |
386 | let expanded = match wide { |
387 | Some(inner) => inner.expand(mem::take(cur), self, state, buf, target_width), |
388 | None => mem::take(cur), |
389 | }; |
390 | |
391 | // If there are newlines, we need to split them up |
392 | // and add the lines separately so that they're counted |
393 | // correctly on re-render. |
394 | for (i, line) in expanded.split('\n ').enumerate() { |
395 | // No newlines found in this case |
396 | if i == 0 && line.len() == expanded.len() { |
397 | lines.push(expanded); |
398 | break; |
399 | } |
400 | |
401 | lines.push(line.to_string()); |
402 | } |
403 | } |
404 | } |
405 | |
406 | struct TabRewriter<'a>(&'a mut dyn fmt::Write, usize); |
407 | |
408 | impl Write for TabRewriter<'_> { |
409 | fn write_str(&mut self, s: &str) -> fmt::Result { |
410 | self.0 |
411 | .write_str(s.replace(from:'\t ', & " ".repeat(self.1)).as_str()) |
412 | } |
413 | } |
414 | |
415 | #[derive(Clone, Copy)] |
416 | enum WideElement<'a> { |
417 | Bar { alt_style: &'a Option<Style> }, |
418 | Message { align: &'a Alignment }, |
419 | } |
420 | |
421 | impl<'a> WideElement<'a> { |
422 | fn expand( |
423 | self, |
424 | cur: String, |
425 | style: &ProgressStyle, |
426 | state: &ProgressState, |
427 | buf: &mut String, |
428 | width: u16, |
429 | ) -> String { |
430 | let left = (width as usize).saturating_sub(measure_text_width(&cur.replace('\x00 ', ""))); |
431 | match self { |
432 | Self::Bar { alt_style } => cur.replace( |
433 | '\x00 ', |
434 | &format!( |
435 | "{} ", |
436 | style.format_bar(state.fraction(), left, alt_style.as_ref()) |
437 | ), |
438 | ), |
439 | WideElement::Message { align } => { |
440 | buf.clear(); |
441 | buf.write_fmt(format_args!( |
442 | "{} ", |
443 | PaddedStringDisplay { |
444 | str: state.message.expanded(), |
445 | width: left, |
446 | align: *align, |
447 | truncate: true, |
448 | } |
449 | )) |
450 | .unwrap(); |
451 | |
452 | let trimmed = match cur.as_bytes().last() == Some(&b'\x00 ') { |
453 | true => buf.trim_end(), |
454 | false => buf, |
455 | }; |
456 | |
457 | cur.replace('\x00 ', trimmed) |
458 | } |
459 | } |
460 | } |
461 | } |
462 | |
463 | #[derive(Clone, Debug)] |
464 | struct Template { |
465 | parts: Vec<TemplatePart>, |
466 | } |
467 | |
468 | impl Template { |
469 | fn from_str_with_tab_width(s: &str, tab_width: usize) -> Result<Self, TemplateError> { |
470 | use State::*; |
471 | let (mut state, mut parts, mut buf) = (Literal, vec![], String::new()); |
472 | for c in s.chars() { |
473 | let new = match (state, c) { |
474 | (Literal, '{') => (MaybeOpen, None), |
475 | (Literal, '\n ') => { |
476 | if !buf.is_empty() { |
477 | parts.push(TemplatePart::Literal(TabExpandedString::new( |
478 | mem::take(&mut buf).into(), |
479 | tab_width, |
480 | ))); |
481 | } |
482 | parts.push(TemplatePart::NewLine); |
483 | (Literal, None) |
484 | } |
485 | (Literal, '}') => (DoubleClose, Some( '}')), |
486 | (Literal, c) => (Literal, Some(c)), |
487 | (DoubleClose, '}') => (Literal, None), |
488 | (MaybeOpen, '{') => (Literal, Some( '{')), |
489 | (MaybeOpen | Key, c) if c.is_ascii_whitespace() => { |
490 | // If we find whitespace where the variable key is supposed to go, |
491 | // backtrack and act as if this was a literal. |
492 | buf.push(c); |
493 | let mut new = String::from("{"); |
494 | new.push_str(&buf); |
495 | buf.clear(); |
496 | parts.push(TemplatePart::Literal(TabExpandedString::new( |
497 | new.into(), |
498 | tab_width, |
499 | ))); |
500 | (Literal, None) |
501 | } |
502 | (MaybeOpen, c) if c != '}'&& c != ':'=> (Key, Some(c)), |
503 | (Key, c) if c != '}'&& c != ':'=> (Key, Some(c)), |
504 | (Key, ':') => (Align, None), |
505 | (Key, '}') => (Literal, None), |
506 | (Key, '!') if !buf.is_empty() => { |
507 | parts.push(TemplatePart::Placeholder { |
508 | key: mem::take(&mut buf), |
509 | align: Alignment::Left, |
510 | width: None, |
511 | truncate: true, |
512 | style: None, |
513 | alt_style: None, |
514 | }); |
515 | (Width, None) |
516 | } |
517 | (Align, c) if c == '<'|| c == '^'|| c == '>'=> { |
518 | if let Some(TemplatePart::Placeholder { align, .. }) = parts.last_mut() { |
519 | match c { |
520 | '<'=> *align = Alignment::Left, |
521 | '^'=> *align = Alignment::Center, |
522 | '>'=> *align = Alignment::Right, |
523 | _ => (), |
524 | } |
525 | } |
526 | |
527 | (Width, None) |
528 | } |
529 | (Align, c @ '0'..= '9') => (Width, Some(c)), |
530 | (Align | Width, '!') => { |
531 | if let Some(TemplatePart::Placeholder { truncate, .. }) = parts.last_mut() { |
532 | *truncate = true; |
533 | } |
534 | (Width, None) |
535 | } |
536 | (Align, '.') => (FirstStyle, None), |
537 | (Align, '}') => (Literal, None), |
538 | (Width, c @ '0'..= '9') => (Width, Some(c)), |
539 | (Width, '.') => (FirstStyle, None), |
540 | (Width, '}') => (Literal, None), |
541 | (FirstStyle, '/') => (AltStyle, None), |
542 | (FirstStyle, '}') => (Literal, None), |
543 | (FirstStyle, c) => (FirstStyle, Some(c)), |
544 | (AltStyle, '}') => (Literal, None), |
545 | (AltStyle, c) => (AltStyle, Some(c)), |
546 | (st, c) => return Err(TemplateError { next: c, state: st }), |
547 | }; |
548 | |
549 | match (state, new.0) { |
550 | (MaybeOpen, Key) if !buf.is_empty() => parts.push(TemplatePart::Literal( |
551 | TabExpandedString::new(mem::take(&mut buf).into(), tab_width), |
552 | )), |
553 | (Key, Align | Literal) if !buf.is_empty() => { |
554 | parts.push(TemplatePart::Placeholder { |
555 | key: mem::take(&mut buf), |
556 | align: Alignment::Left, |
557 | width: None, |
558 | truncate: false, |
559 | style: None, |
560 | alt_style: None, |
561 | }); |
562 | } |
563 | (Width, FirstStyle | Literal) if !buf.is_empty() => { |
564 | if let Some(TemplatePart::Placeholder { width, .. }) = parts.last_mut() { |
565 | *width = Some(buf.parse().unwrap()); |
566 | buf.clear(); |
567 | } |
568 | } |
569 | (FirstStyle, AltStyle | Literal) if !buf.is_empty() => { |
570 | if let Some(TemplatePart::Placeholder { style, .. }) = parts.last_mut() { |
571 | *style = Some(Style::from_dotted_str(&buf)); |
572 | buf.clear(); |
573 | } |
574 | } |
575 | (AltStyle, Literal) if !buf.is_empty() => { |
576 | if let Some(TemplatePart::Placeholder { alt_style, .. }) = parts.last_mut() { |
577 | *alt_style = Some(Style::from_dotted_str(&buf)); |
578 | buf.clear(); |
579 | } |
580 | } |
581 | (_, _) => (), |
582 | } |
583 | |
584 | state = new.0; |
585 | if let Some(c) = new.1 { |
586 | buf.push(c); |
587 | } |
588 | } |
589 | |
590 | if matches!(state, Literal | DoubleClose) && !buf.is_empty() { |
591 | parts.push(TemplatePart::Literal(TabExpandedString::new( |
592 | buf.into(), |
593 | tab_width, |
594 | ))); |
595 | } |
596 | |
597 | Ok(Self { parts }) |
598 | } |
599 | |
600 | fn from_str(s: &str) -> Result<Self, TemplateError> { |
601 | Self::from_str_with_tab_width(s, DEFAULT_TAB_WIDTH) |
602 | } |
603 | |
604 | fn set_tab_width(&mut self, new_tab_width: usize) { |
605 | for part in &mut self.parts { |
606 | if let TemplatePart::Literal(s) = part { |
607 | s.set_tab_width(new_tab_width); |
608 | } |
609 | } |
610 | } |
611 | } |
612 | |
613 | #[derive(Debug)] |
614 | pub struct TemplateError { |
615 | state: State, |
616 | next: char, |
617 | } |
618 | |
619 | impl fmt::Display for TemplateError { |
620 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
621 | write!( |
622 | f, |
623 | "TemplateError: unexpected character{:?} in state{:?} ", |
624 | self.next, self.state |
625 | ) |
626 | } |
627 | } |
628 | |
629 | impl std::error::Error for TemplateError {} |
630 | |
631 | #[derive(Clone, Debug, PartialEq, Eq)] |
632 | enum TemplatePart { |
633 | Literal(TabExpandedString), |
634 | Placeholder { |
635 | key: String, |
636 | align: Alignment, |
637 | width: Option<u16>, |
638 | truncate: bool, |
639 | style: Option<Style>, |
640 | alt_style: Option<Style>, |
641 | }, |
642 | NewLine, |
643 | } |
644 | |
645 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
646 | enum State { |
647 | Literal, |
648 | MaybeOpen, |
649 | DoubleClose, |
650 | Key, |
651 | Align, |
652 | Width, |
653 | FirstStyle, |
654 | AltStyle, |
655 | } |
656 | |
657 | struct BarDisplay<'a> { |
658 | chars: &'a [Box<str>], |
659 | filled: usize, |
660 | cur: Option<usize>, |
661 | rest: console::StyledObject<RepeatedStringDisplay<'a>>, |
662 | } |
663 | |
664 | impl<'a> fmt::Display for BarDisplay<'a> { |
665 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
666 | for _ in 0..self.filled { |
667 | f.write_str(&self.chars[0])?; |
668 | } |
669 | if let Some(cur: usize) = self.cur { |
670 | f.write_str(&self.chars[cur])?; |
671 | } |
672 | self.rest.fmt(f) |
673 | } |
674 | } |
675 | |
676 | struct RepeatedStringDisplay<'a> { |
677 | str: &'a str, |
678 | num: usize, |
679 | } |
680 | |
681 | impl<'a> fmt::Display for RepeatedStringDisplay<'a> { |
682 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
683 | for _ in 0..self.num { |
684 | f.write_str(self.str)?; |
685 | } |
686 | Ok(()) |
687 | } |
688 | } |
689 | |
690 | struct PaddedStringDisplay<'a> { |
691 | str: &'a str, |
692 | width: usize, |
693 | align: Alignment, |
694 | truncate: bool, |
695 | } |
696 | |
697 | impl<'a> fmt::Display for PaddedStringDisplay<'a> { |
698 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
699 | let cols = measure_text_width(self.str); |
700 | let excess = cols.saturating_sub(self.width); |
701 | if excess > 0 && !self.truncate { |
702 | return f.write_str(self.str); |
703 | } else if excess > 0 { |
704 | let (start, end) = match self.align { |
705 | Alignment::Left => (0, self.str.len() - excess), |
706 | Alignment::Right => (excess, self.str.len()), |
707 | Alignment::Center => ( |
708 | excess / 2, |
709 | self.str.len() - excess.saturating_sub(excess / 2), |
710 | ), |
711 | }; |
712 | |
713 | return f.write_str(self.str.get(start..end).unwrap_or(self.str)); |
714 | } |
715 | |
716 | let diff = self.width.saturating_sub(cols); |
717 | let (left_pad, right_pad) = match self.align { |
718 | Alignment::Left => (0, diff), |
719 | Alignment::Right => (diff, 0), |
720 | Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)), |
721 | }; |
722 | |
723 | for _ in 0..left_pad { |
724 | f.write_char(' ')?; |
725 | } |
726 | f.write_str(self.str)?; |
727 | for _ in 0..right_pad { |
728 | f.write_char(' ')?; |
729 | } |
730 | Ok(()) |
731 | } |
732 | } |
733 | |
734 | #[derive(PartialEq, Eq, Debug, Copy, Clone)] |
735 | enum Alignment { |
736 | Left, |
737 | Center, |
738 | Right, |
739 | } |
740 | |
741 | /// Trait for defining stateful or stateless formatters |
742 | pub trait ProgressTracker: Send + Sync { |
743 | /// Creates a new instance of the progress tracker |
744 | fn clone_box(&self) -> Box<dyn ProgressTracker>; |
745 | /// Notifies the progress tracker of a tick event |
746 | fn tick(&mut self, state: &ProgressState, now: Instant); |
747 | /// Notifies the progress tracker of a reset event |
748 | fn reset(&mut self, state: &ProgressState, now: Instant); |
749 | /// Provides access to the progress bar display buffer for custom messages |
750 | fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write); |
751 | } |
752 | |
753 | impl Clone for Box<dyn ProgressTracker> { |
754 | fn clone(&self) -> Self { |
755 | self.clone_box() |
756 | } |
757 | } |
758 | |
759 | impl<F> ProgressTracker for F |
760 | where |
761 | F: Fn(&ProgressState, &mut dyn fmt::Write) + Send + Sync + Clone + 'static, |
762 | { |
763 | fn clone_box(&self) -> Box<dyn ProgressTracker> { |
764 | Box::new(self.clone()) |
765 | } |
766 | |
767 | fn tick(&mut self, _: &ProgressState, _: Instant) {} |
768 | |
769 | fn reset(&mut self, _: &ProgressState, _: Instant) {} |
770 | |
771 | fn write(&self, state: &ProgressState, w: &mut dyn fmt::Write) { |
772 | (self)(state, w); |
773 | } |
774 | } |
775 | |
776 | #[cfg(test)] |
777 | mod tests { |
778 | use std::sync::Arc; |
779 | |
780 | use super::*; |
781 | use crate::state::{AtomicPosition, ProgressState}; |
782 | |
783 | use console::set_colors_enabled; |
784 | use std::sync::Mutex; |
785 | |
786 | #[test] |
787 | fn test_stateful_tracker() { |
788 | #[derive(Debug, Clone)] |
789 | struct TestTracker(Arc<Mutex<String>>); |
790 | |
791 | impl ProgressTracker for TestTracker { |
792 | fn clone_box(&self) -> Box<dyn ProgressTracker> { |
793 | Box::new(self.clone()) |
794 | } |
795 | |
796 | fn tick(&mut self, state: &ProgressState, _: Instant) { |
797 | let mut m = self.0.lock().unwrap(); |
798 | m.clear(); |
799 | m.push_str(format!("{} {}", state.len().unwrap(), state.pos()).as_str()); |
800 | } |
801 | |
802 | fn reset(&mut self, _state: &ProgressState, _: Instant) { |
803 | let mut m = self.0.lock().unwrap(); |
804 | m.clear(); |
805 | } |
806 | |
807 | fn write(&self, _state: &ProgressState, w: &mut dyn fmt::Write) { |
808 | w.write_str(self.0.lock().unwrap().as_str()).unwrap(); |
809 | } |
810 | } |
811 | |
812 | use crate::ProgressBar; |
813 | |
814 | let pb = ProgressBar::new(1); |
815 | pb.set_style( |
816 | ProgressStyle::with_template("{{ {foo} }}") |
817 | .unwrap() |
818 | .with_key("foo", TestTracker(Arc::new(Mutex::new(String::default())))) |
819 | .progress_chars("#>-"), |
820 | ); |
821 | |
822 | let mut buf = Vec::new(); |
823 | let style = pb.clone().style(); |
824 | |
825 | style.format_state(&pb.state().state, &mut buf, 16); |
826 | assert_eq!(&buf[0], "{ }"); |
827 | buf.clear(); |
828 | pb.inc(1); |
829 | style.format_state(&pb.state().state, &mut buf, 16); |
830 | assert_eq!(&buf[0], "{ 1 1 }"); |
831 | pb.reset(); |
832 | buf.clear(); |
833 | style.format_state(&pb.state().state, &mut buf, 16); |
834 | assert_eq!(&buf[0], "{ }"); |
835 | pb.finish_and_clear(); |
836 | } |
837 | |
838 | use crate::state::TabExpandedString; |
839 | |
840 | #[test] |
841 | fn test_expand_template() { |
842 | const WIDTH: u16 = 80; |
843 | let pos = Arc::new(AtomicPosition::new()); |
844 | let state = ProgressState::new(Some(10), pos); |
845 | let mut buf = Vec::new(); |
846 | |
847 | let mut style = ProgressStyle::default_bar(); |
848 | style.format_map.insert( |
849 | "foo", |
850 | Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "FOO").unwrap()), |
851 | ); |
852 | style.format_map.insert( |
853 | "bar", |
854 | Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "BAR").unwrap()), |
855 | ); |
856 | |
857 | style.template = Template::from_str("{{ {foo} {bar} }}").unwrap(); |
858 | style.format_state(&state, &mut buf, WIDTH); |
859 | assert_eq!(&buf[0], "{ FOO BAR }"); |
860 | |
861 | buf.clear(); |
862 | style.template = Template::from_str(r#"{ "foo": "{foo}", "bar": {bar} }"#).unwrap(); |
863 | style.format_state(&state, &mut buf, WIDTH); |
864 | assert_eq!(&buf[0], r#"{ "foo": "FOO", "bar": BAR }"#); |
865 | } |
866 | |
867 | #[test] |
868 | fn test_expand_template_flags() { |
869 | set_colors_enabled(true); |
870 | |
871 | const WIDTH: u16 = 80; |
872 | let pos = Arc::new(AtomicPosition::new()); |
873 | let state = ProgressState::new(Some(10), pos); |
874 | let mut buf = Vec::new(); |
875 | |
876 | let mut style = ProgressStyle::default_bar(); |
877 | style.format_map.insert( |
878 | "foo", |
879 | Box::new(|_: &ProgressState, w: &mut dyn Write| write!(w, "XXX").unwrap()), |
880 | ); |
881 | |
882 | style.template = Template::from_str("{foo:5}").unwrap(); |
883 | style.format_state(&state, &mut buf, WIDTH); |
884 | assert_eq!(&buf[0], "XXX "); |
885 | |
886 | buf.clear(); |
887 | style.template = Template::from_str("{foo:.red.on_blue}").unwrap(); |
888 | style.format_state(&state, &mut buf, WIDTH); |
889 | assert_eq!(&buf[0], "\u{1b} [31m\u{1b} [44mXXX\u{1b} [0m"); |
890 | |
891 | buf.clear(); |
892 | style.template = Template::from_str("{foo:^5.red.on_blue}").unwrap(); |
893 | style.format_state(&state, &mut buf, WIDTH); |
894 | assert_eq!(&buf[0], "\u{1b} [31m\u{1b} [44m XXX\u{1b} [0m"); |
895 | |
896 | buf.clear(); |
897 | style.template = Template::from_str("{foo:^5.red.on_blue/green.on_cyan}").unwrap(); |
898 | style.format_state(&state, &mut buf, WIDTH); |
899 | assert_eq!(&buf[0], "\u{1b} [31m\u{1b} [44m XXX\u{1b} [0m"); |
900 | } |
901 | |
902 | #[test] |
903 | fn align_truncation() { |
904 | const WIDTH: u16 = 10; |
905 | let pos = Arc::new(AtomicPosition::new()); |
906 | let mut state = ProgressState::new(Some(10), pos); |
907 | let mut buf = Vec::new(); |
908 | |
909 | let style = ProgressStyle::with_template("{wide_msg}").unwrap(); |
910 | state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into()); |
911 | style.format_state(&state, &mut buf, WIDTH); |
912 | assert_eq!(&buf[0], "abcdefghij"); |
913 | |
914 | buf.clear(); |
915 | let style = ProgressStyle::with_template("{wide_msg:>}").unwrap(); |
916 | state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into()); |
917 | style.format_state(&state, &mut buf, WIDTH); |
918 | assert_eq!(&buf[0], "klmnopqrst"); |
919 | |
920 | buf.clear(); |
921 | let style = ProgressStyle::with_template("{wide_msg:^}").unwrap(); |
922 | state.message = TabExpandedString::NoTabs("abcdefghijklmnopqrst".into()); |
923 | style.format_state(&state, &mut buf, WIDTH); |
924 | assert_eq!(&buf[0], "fghijklmno"); |
925 | } |
926 | |
927 | #[test] |
928 | fn wide_element_style() { |
929 | set_colors_enabled(true); |
930 | |
931 | const CHARS: &str = "=>-"; |
932 | const WIDTH: u16 = 8; |
933 | let pos = Arc::new(AtomicPosition::new()); |
934 | // half finished |
935 | pos.set(2); |
936 | let mut state = ProgressState::new(Some(4), pos); |
937 | let mut buf = Vec::new(); |
938 | |
939 | let style = ProgressStyle::with_template("{wide_bar}") |
940 | .unwrap() |
941 | .progress_chars(CHARS); |
942 | style.format_state(&state, &mut buf, WIDTH); |
943 | assert_eq!(&buf[0], "====>---"); |
944 | |
945 | buf.clear(); |
946 | let style = ProgressStyle::with_template("{wide_bar:.red.on_blue/green.on_cyan}") |
947 | .unwrap() |
948 | .progress_chars(CHARS); |
949 | style.format_state(&state, &mut buf, WIDTH); |
950 | assert_eq!( |
951 | &buf[0], |
952 | "\u{1b} [31m\u{1b} [44m====>\u{1b} [32m\u{1b} [46m---\u{1b} [0m\u{1b} [0m" |
953 | ); |
954 | |
955 | buf.clear(); |
956 | let style = ProgressStyle::with_template("{wide_msg:^.red.on_blue}").unwrap(); |
957 | state.message = TabExpandedString::NoTabs("foobar".into()); |
958 | style.format_state(&state, &mut buf, WIDTH); |
959 | assert_eq!(&buf[0], "\u{1b} [31m\u{1b} [44m foobar\u{1b} [0m"); |
960 | } |
961 | |
962 | #[test] |
963 | fn multiline_handling() { |
964 | const WIDTH: u16 = 80; |
965 | let pos = Arc::new(AtomicPosition::new()); |
966 | let mut state = ProgressState::new(Some(10), pos); |
967 | let mut buf = Vec::new(); |
968 | |
969 | let mut style = ProgressStyle::default_bar(); |
970 | state.message = TabExpandedString::new("foo\n bar\n baz".into(), 2); |
971 | style.template = Template::from_str("{msg}").unwrap(); |
972 | style.format_state(&state, &mut buf, WIDTH); |
973 | |
974 | assert_eq!(buf.len(), 3); |
975 | assert_eq!(&buf[0], "foo"); |
976 | assert_eq!(&buf[1], "bar"); |
977 | assert_eq!(&buf[2], "baz"); |
978 | |
979 | buf.clear(); |
980 | style.template = Template::from_str("{wide_msg}").unwrap(); |
981 | style.format_state(&state, &mut buf, WIDTH); |
982 | |
983 | assert_eq!(buf.len(), 3); |
984 | assert_eq!(&buf[0], "foo"); |
985 | assert_eq!(&buf[1], "bar"); |
986 | assert_eq!(&buf[2], "baz"); |
987 | |
988 | buf.clear(); |
989 | state.prefix = TabExpandedString::new("prefix\n prefix".into(), 2); |
990 | style.template = Template::from_str("{prefix} {wide_msg}").unwrap(); |
991 | style.format_state(&state, &mut buf, WIDTH); |
992 | |
993 | assert_eq!(buf.len(), 4); |
994 | assert_eq!(&buf[0], "prefix"); |
995 | assert_eq!(&buf[1], "prefix foo"); |
996 | assert_eq!(&buf[2], "bar"); |
997 | assert_eq!(&buf[3], "baz"); |
998 | } |
999 | } |
1000 |
Definitions
- ProgressStyle
- tick_strings
- progress_chars
- template
- char_width
- tab_width
- format_map
- segment
- measure
- width
- default_bar
- default_spinner
- with_template
- set_tab_width
- new
- tick_chars
- tick_strings
- progress_chars
- with_key
- template
- current_tick_str
- get_tick_str
- get_final_tick_str
- format_bar
- format_state
- key
- align
- width
- truncate
- style
- alt_style
- push_line
- TabRewriter
- write_str
- WideElement
- Bar
- alt_style
- Message
- align
- expand
- alt_style
- align
- Template
- parts
- from_str_with_tab_width
- align
- truncate
- width
- style
- alt_style
- from_str
- set_tab_width
- TemplateError
- state
- next
- fmt
- TemplatePart
- Literal
- Placeholder
- key
- align
- width
- truncate
- style
- alt_style
- NewLine
- State
- Literal
- MaybeOpen
- DoubleClose
- Key
- Align
- Width
- FirstStyle
- AltStyle
- BarDisplay
- chars
- filled
- cur
- rest
- fmt
- RepeatedStringDisplay
- str
- num
- fmt
- PaddedStringDisplay
- str
- width
- align
- truncate
- fmt
- Alignment
- Left
- Center
- Right
- ProgressTracker
- clone_box
- tick
- reset
- write
- clone
- clone_box
- tick
- reset
Learn Rust with the experts
Find out more