1 | use std::io; |
2 | use std::sync::{Arc, RwLock, RwLockWriteGuard}; |
3 | use std::thread::panicking; |
4 | use std::time::Duration; |
5 | #[cfg (not(target_arch = "wasm32" ))] |
6 | use std::time::Instant; |
7 | |
8 | use console::Term; |
9 | #[cfg (target_arch = "wasm32" )] |
10 | use instant::Instant; |
11 | |
12 | use crate::multi::{MultiProgressAlignment, MultiState}; |
13 | use crate::TermLike; |
14 | |
15 | /// Target for draw operations |
16 | /// |
17 | /// This tells a progress bar or a multi progress object where to paint to. |
18 | /// The draw target is a stateful wrapper over a drawing destination and |
19 | /// internally optimizes how often the state is painted to the output |
20 | /// device. |
21 | #[derive (Debug)] |
22 | pub struct ProgressDrawTarget { |
23 | kind: TargetKind, |
24 | } |
25 | |
26 | impl ProgressDrawTarget { |
27 | /// Draw to a buffered stdout terminal at a max of 20 times a second. |
28 | /// |
29 | /// For more information see [`ProgressDrawTarget::term`]. |
30 | pub fn stdout() -> Self { |
31 | Self::term(Term::buffered_stdout(), 20) |
32 | } |
33 | |
34 | /// Draw to a buffered stderr terminal at a max of 20 times a second. |
35 | /// |
36 | /// This is the default draw target for progress bars. For more |
37 | /// information see [`ProgressDrawTarget::term`]. |
38 | pub fn stderr() -> Self { |
39 | Self::term(Term::buffered_stderr(), 20) |
40 | } |
41 | |
42 | /// Draw to a buffered stdout terminal at a max of `refresh_rate` times a second. |
43 | /// |
44 | /// For more information see [`ProgressDrawTarget::term`]. |
45 | pub fn stdout_with_hz(refresh_rate: u8) -> Self { |
46 | Self::term(Term::buffered_stdout(), refresh_rate) |
47 | } |
48 | |
49 | /// Draw to a buffered stderr terminal at a max of `refresh_rate` times a second. |
50 | /// |
51 | /// For more information see [`ProgressDrawTarget::term`]. |
52 | pub fn stderr_with_hz(refresh_rate: u8) -> Self { |
53 | Self::term(Term::buffered_stderr(), refresh_rate) |
54 | } |
55 | |
56 | pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self { |
57 | Self { |
58 | kind: TargetKind::Multi { state, idx }, |
59 | } |
60 | } |
61 | |
62 | /// Draw to a terminal, with a specific refresh rate. |
63 | /// |
64 | /// Progress bars are by default drawn to terminals however if the |
65 | /// terminal is not user attended the entire progress bar will be |
66 | /// hidden. This is done so that piping to a file will not produce |
67 | /// useless escape codes in that file. |
68 | /// |
69 | /// Will panic if refresh_rate is `0`. |
70 | pub fn term(term: Term, refresh_rate: u8) -> Self { |
71 | Self { |
72 | kind: TargetKind::Term { |
73 | term, |
74 | last_line_count: 0, |
75 | rate_limiter: RateLimiter::new(refresh_rate), |
76 | draw_state: DrawState::default(), |
77 | }, |
78 | } |
79 | } |
80 | |
81 | /// Draw to a boxed object that implements the [`TermLike`] trait. |
82 | pub fn term_like(term_like: Box<dyn TermLike>) -> Self { |
83 | Self { |
84 | kind: TargetKind::TermLike { |
85 | inner: term_like, |
86 | last_line_count: 0, |
87 | rate_limiter: None, |
88 | draw_state: DrawState::default(), |
89 | }, |
90 | } |
91 | } |
92 | |
93 | /// Draw to a boxed object that implements the [`TermLike`] trait, |
94 | /// with a specific refresh rate. |
95 | pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self { |
96 | Self { |
97 | kind: TargetKind::TermLike { |
98 | inner: term_like, |
99 | last_line_count: 0, |
100 | rate_limiter: Option::from(RateLimiter::new(refresh_rate)), |
101 | draw_state: DrawState::default(), |
102 | }, |
103 | } |
104 | } |
105 | |
106 | /// A hidden draw target. |
107 | /// |
108 | /// This forces a progress bar to be not rendered at all. |
109 | pub fn hidden() -> Self { |
110 | Self { |
111 | kind: TargetKind::Hidden, |
112 | } |
113 | } |
114 | |
115 | /// Returns true if the draw target is hidden. |
116 | /// |
117 | /// This is internally used in progress bars to figure out if overhead |
118 | /// from drawing can be prevented. |
119 | pub fn is_hidden(&self) -> bool { |
120 | match self.kind { |
121 | TargetKind::Hidden => true, |
122 | TargetKind::Term { ref term, .. } => !term.is_term(), |
123 | TargetKind::Multi { ref state, .. } => state.read().unwrap().is_hidden(), |
124 | _ => false, |
125 | } |
126 | } |
127 | |
128 | /// Returns the current width of the draw target. |
129 | pub(crate) fn width(&self) -> u16 { |
130 | match self.kind { |
131 | TargetKind::Term { ref term, .. } => term.size().1, |
132 | TargetKind::Multi { ref state, .. } => state.read().unwrap().width(), |
133 | TargetKind::Hidden => 0, |
134 | TargetKind::TermLike { ref inner, .. } => inner.width(), |
135 | } |
136 | } |
137 | |
138 | /// Notifies the backing `MultiProgress` (if applicable) that the associated progress bar should |
139 | /// be marked a zombie. |
140 | pub(crate) fn mark_zombie(&self) { |
141 | if let TargetKind::Multi { idx, state } = &self.kind { |
142 | state.write().unwrap().mark_zombie(*idx); |
143 | } |
144 | } |
145 | |
146 | /// Apply the given draw state (draws it). |
147 | pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> { |
148 | match &mut self.kind { |
149 | TargetKind::Term { |
150 | term, |
151 | last_line_count, |
152 | rate_limiter, |
153 | draw_state, |
154 | } => { |
155 | if !term.is_term() { |
156 | return None; |
157 | } |
158 | |
159 | match force_draw || rate_limiter.allow(now) { |
160 | true => Some(Drawable::Term { |
161 | term, |
162 | last_line_count, |
163 | draw_state, |
164 | }), |
165 | false => None, // rate limited |
166 | } |
167 | } |
168 | TargetKind::Multi { idx, state, .. } => { |
169 | let state = state.write().unwrap(); |
170 | Some(Drawable::Multi { |
171 | idx: *idx, |
172 | state, |
173 | force_draw, |
174 | now, |
175 | }) |
176 | } |
177 | TargetKind::TermLike { |
178 | inner, |
179 | last_line_count, |
180 | rate_limiter, |
181 | draw_state, |
182 | } => match force_draw || rate_limiter.as_mut().map_or(true, |r| r.allow(now)) { |
183 | true => Some(Drawable::TermLike { |
184 | term_like: &**inner, |
185 | last_line_count, |
186 | draw_state, |
187 | }), |
188 | false => None, // rate limited |
189 | }, |
190 | // Hidden, finished, or no need to refresh yet |
191 | _ => None, |
192 | } |
193 | } |
194 | |
195 | /// Properly disconnects from the draw target |
196 | pub(crate) fn disconnect(&self, now: Instant) { |
197 | match self.kind { |
198 | TargetKind::Term { .. } => {} |
199 | TargetKind::Multi { idx, ref state, .. } => { |
200 | let state = state.write().unwrap(); |
201 | let _ = Drawable::Multi { |
202 | state, |
203 | idx, |
204 | force_draw: true, |
205 | now, |
206 | } |
207 | .clear(); |
208 | } |
209 | TargetKind::Hidden => {} |
210 | TargetKind::TermLike { .. } => {} |
211 | }; |
212 | } |
213 | |
214 | pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> { |
215 | match &self.kind { |
216 | TargetKind::Multi { state, idx } => Some((state, *idx)), |
217 | _ => None, |
218 | } |
219 | } |
220 | |
221 | pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) { |
222 | self.kind.adjust_last_line_count(adjust); |
223 | } |
224 | } |
225 | |
226 | #[derive (Debug)] |
227 | enum TargetKind { |
228 | Term { |
229 | term: Term, |
230 | last_line_count: usize, |
231 | rate_limiter: RateLimiter, |
232 | draw_state: DrawState, |
233 | }, |
234 | Multi { |
235 | state: Arc<RwLock<MultiState>>, |
236 | idx: usize, |
237 | }, |
238 | Hidden, |
239 | TermLike { |
240 | inner: Box<dyn TermLike>, |
241 | last_line_count: usize, |
242 | rate_limiter: Option<RateLimiter>, |
243 | draw_state: DrawState, |
244 | }, |
245 | } |
246 | |
247 | impl TargetKind { |
248 | /// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines |
249 | fn adjust_last_line_count(&mut self, adjust: LineAdjust) { |
250 | let last_line_count: &mut usize = match self { |
251 | Self::Term { |
252 | last_line_count: &mut usize, .. |
253 | } => last_line_count, |
254 | Self::TermLike { |
255 | last_line_count: &mut usize, .. |
256 | } => last_line_count, |
257 | _ => return, |
258 | }; |
259 | |
260 | match adjust { |
261 | LineAdjust::Clear(count: usize) => *last_line_count = last_line_count.saturating_add(count), |
262 | LineAdjust::Keep(count: usize) => *last_line_count = last_line_count.saturating_sub(count), |
263 | } |
264 | } |
265 | } |
266 | |
267 | pub(crate) enum Drawable<'a> { |
268 | Term { |
269 | term: &'a Term, |
270 | last_line_count: &'a mut usize, |
271 | draw_state: &'a mut DrawState, |
272 | }, |
273 | Multi { |
274 | state: RwLockWriteGuard<'a, MultiState>, |
275 | idx: usize, |
276 | force_draw: bool, |
277 | now: Instant, |
278 | }, |
279 | TermLike { |
280 | term_like: &'a dyn TermLike, |
281 | last_line_count: &'a mut usize, |
282 | draw_state: &'a mut DrawState, |
283 | }, |
284 | } |
285 | |
286 | impl<'a> Drawable<'a> { |
287 | /// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines |
288 | pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) { |
289 | let last_line_count: &mut usize = match self { |
290 | Drawable::Term { |
291 | last_line_count, .. |
292 | } => last_line_count, |
293 | Drawable::TermLike { |
294 | last_line_count, .. |
295 | } => last_line_count, |
296 | _ => return, |
297 | }; |
298 | |
299 | match adjust { |
300 | LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count), |
301 | LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count), |
302 | } |
303 | } |
304 | |
305 | pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> { |
306 | let mut state = match self { |
307 | Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state), |
308 | Drawable::Multi { state, idx, .. } => state.draw_state(*idx), |
309 | Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state), |
310 | }; |
311 | |
312 | state.reset(); |
313 | state |
314 | } |
315 | |
316 | pub(crate) fn clear(mut self) -> io::Result<()> { |
317 | let state = self.state(); |
318 | drop(state); |
319 | self.draw() |
320 | } |
321 | |
322 | pub(crate) fn draw(self) -> io::Result<()> { |
323 | match self { |
324 | Drawable::Term { |
325 | term, |
326 | last_line_count, |
327 | draw_state, |
328 | } => draw_state.draw_to_term(term, last_line_count), |
329 | Drawable::Multi { |
330 | mut state, |
331 | force_draw, |
332 | now, |
333 | .. |
334 | } => state.draw(force_draw, None, now), |
335 | Drawable::TermLike { |
336 | term_like, |
337 | last_line_count, |
338 | draw_state, |
339 | } => draw_state.draw_to_term(term_like, last_line_count), |
340 | } |
341 | } |
342 | } |
343 | |
344 | pub(crate) enum LineAdjust { |
345 | /// Adds to `last_line_count` so that the next draw also clears those lines |
346 | Clear(usize), |
347 | /// Subtracts from `last_line_count` so that the next draw retains those lines |
348 | Keep(usize), |
349 | } |
350 | |
351 | pub(crate) struct DrawStateWrapper<'a> { |
352 | state: &'a mut DrawState, |
353 | orphan_lines: Option<&'a mut Vec<String>>, |
354 | } |
355 | |
356 | impl<'a> DrawStateWrapper<'a> { |
357 | pub(crate) fn for_term(state: &'a mut DrawState) -> Self { |
358 | Self { |
359 | state, |
360 | orphan_lines: None, |
361 | } |
362 | } |
363 | |
364 | pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<String>) -> Self { |
365 | Self { |
366 | state, |
367 | orphan_lines: Some(orphan_lines), |
368 | } |
369 | } |
370 | } |
371 | |
372 | impl std::ops::Deref for DrawStateWrapper<'_> { |
373 | type Target = DrawState; |
374 | |
375 | fn deref(&self) -> &Self::Target { |
376 | self.state |
377 | } |
378 | } |
379 | |
380 | impl std::ops::DerefMut for DrawStateWrapper<'_> { |
381 | fn deref_mut(&mut self) -> &mut Self::Target { |
382 | self.state |
383 | } |
384 | } |
385 | |
386 | impl Drop for DrawStateWrapper<'_> { |
387 | fn drop(&mut self) { |
388 | if let Some(orphaned: &mut &mut Vec) = &mut self.orphan_lines { |
389 | orphaned.extend(self.state.lines.drain(..self.state.orphan_lines_count)); |
390 | self.state.orphan_lines_count = 0; |
391 | } |
392 | } |
393 | } |
394 | |
395 | #[derive (Debug)] |
396 | struct RateLimiter { |
397 | interval: u16, // in milliseconds |
398 | capacity: u8, |
399 | prev: Instant, |
400 | } |
401 | |
402 | /// Rate limit but allow occasional bursts above desired rate |
403 | impl RateLimiter { |
404 | fn new(rate: u8) -> Self { |
405 | Self { |
406 | interval: 1000 / (rate as u16), // between 3 and 1000 milliseconds |
407 | capacity: MAX_BURST, |
408 | prev: Instant::now(), |
409 | } |
410 | } |
411 | |
412 | fn allow(&mut self, now: Instant) -> bool { |
413 | if now < self.prev { |
414 | return false; |
415 | } |
416 | |
417 | let elapsed = now - self.prev; |
418 | // If `capacity` is 0 and not enough time (`self.interval` ms) has passed since |
419 | // `self.prev` to add new capacity, return `false`. The goal of this method is to |
420 | // make this decision as efficient as possible. |
421 | if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) { |
422 | return false; |
423 | } |
424 | |
425 | // We now calculate `new`, the number of ms, since we last returned `true`, |
426 | // and `remainder`, which represents a number of ns less than 1ms which we cannot |
427 | // convert into capacity now, so we're saving it for later. |
428 | let (new, remainder) = ( |
429 | elapsed.as_millis() / self.interval as u128, |
430 | elapsed.as_nanos() % (self.interval as u128 * 1_000_000), |
431 | ); |
432 | |
433 | // We add `new` to `capacity`, subtract one for returning `true` from here, |
434 | // then make sure it does not exceed a maximum of `MAX_BURST`, then store it. |
435 | self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8; |
436 | // Store `prev` for the next iteration after subtracting the `remainder`. |
437 | // Just use `unwrap` here because it shouldn't be possible for this to underflow. |
438 | self.prev = now |
439 | .checked_sub(Duration::from_nanos(remainder as u64)) |
440 | .unwrap(); |
441 | true |
442 | } |
443 | } |
444 | |
445 | const MAX_BURST: u8 = 20; |
446 | |
447 | /// The drawn state of an element. |
448 | #[derive (Clone, Debug, Default)] |
449 | pub(crate) struct DrawState { |
450 | /// The lines to print (can contain ANSI codes) |
451 | pub(crate) lines: Vec<String>, |
452 | /// The number of lines that shouldn't be reaped by the next tick. |
453 | pub(crate) orphan_lines_count: usize, |
454 | /// True if we should move the cursor up when possible instead of clearing lines. |
455 | pub(crate) move_cursor: bool, |
456 | /// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top` |
457 | pub(crate) alignment: MultiProgressAlignment, |
458 | } |
459 | |
460 | impl DrawState { |
461 | fn draw_to_term( |
462 | &mut self, |
463 | term: &(impl TermLike + ?Sized), |
464 | last_line_count: &mut usize, |
465 | ) -> io::Result<()> { |
466 | if panicking() { |
467 | return Ok(()); |
468 | } |
469 | |
470 | if !self.lines.is_empty() && self.move_cursor { |
471 | term.move_cursor_up(*last_line_count)?; |
472 | } else { |
473 | // Fork of console::clear_last_lines that assumes that the last line doesn't contain a '\n' |
474 | let n = *last_line_count; |
475 | term.move_cursor_up(n.saturating_sub(1))?; |
476 | for i in 0..n { |
477 | term.clear_line()?; |
478 | if i + 1 != n { |
479 | term.move_cursor_down(1)?; |
480 | } |
481 | } |
482 | term.move_cursor_up(n.saturating_sub(1))?; |
483 | } |
484 | |
485 | let shift = match self.alignment { |
486 | MultiProgressAlignment::Bottom if self.lines.len() < *last_line_count => { |
487 | let shift = *last_line_count - self.lines.len(); |
488 | for _ in 0..shift { |
489 | term.write_line("" )?; |
490 | } |
491 | shift |
492 | } |
493 | _ => 0, |
494 | }; |
495 | |
496 | let term_height = term.height() as usize; |
497 | let term_width = term.width() as usize; |
498 | let len = self.lines.len(); |
499 | let mut real_len = 0; |
500 | let mut last_line_filler = 0; |
501 | for (idx, line) in self.lines.iter().enumerate() { |
502 | let line_width = console::measure_text_width(line); |
503 | let diff = if line.is_empty() { |
504 | // Empty line are new line |
505 | 1 |
506 | } else { |
507 | // Calculate real length based on terminal width |
508 | // This take in account linewrap from terminal |
509 | let terminal_len = (line_width as f64 / term_width as f64).ceil() as usize; |
510 | |
511 | // If the line is effectively empty (for example when it consists |
512 | // solely of ANSI color code sequences, count it the same as a |
513 | // new line. If the line is measured to be len = 0, we will |
514 | // subtract with overflow later. |
515 | usize::max(terminal_len, 1) |
516 | }; |
517 | if real_len + diff > term_height { |
518 | break; |
519 | } |
520 | real_len += diff; |
521 | if idx != 0 { |
522 | term.write_line("" )?; |
523 | } |
524 | term.write_str(line)?; |
525 | if idx + 1 == len { |
526 | // Keep the cursor on the right terminal side |
527 | // So that next user writes/prints will happen on the next line |
528 | last_line_filler = term_width.saturating_sub(line_width); |
529 | } |
530 | } |
531 | term.write_str(&" " .repeat(last_line_filler))?; |
532 | |
533 | term.flush()?; |
534 | *last_line_count = real_len - self.orphan_lines_count + shift; |
535 | Ok(()) |
536 | } |
537 | |
538 | fn reset(&mut self) { |
539 | self.lines.clear(); |
540 | self.orphan_lines_count = 0; |
541 | } |
542 | } |
543 | |
544 | #[cfg (test)] |
545 | mod tests { |
546 | use crate::{MultiProgress, ProgressBar, ProgressDrawTarget}; |
547 | |
548 | #[test ] |
549 | fn multi_is_hidden() { |
550 | let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden()); |
551 | |
552 | let pb = mp.add(ProgressBar::new(100)); |
553 | assert!(mp.is_hidden()); |
554 | assert!(pb.is_hidden()); |
555 | } |
556 | } |
557 | |