1use std::io;
2use std::sync::{Arc, RwLock, RwLockWriteGuard};
3use std::thread::panicking;
4use std::time::Duration;
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Instant;
7
8use console::Term;
9#[cfg(target_arch = "wasm32")]
10use instant::Instant;
11
12use crate::multi::{MultiProgressAlignment, MultiState};
13use 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)]
22pub struct ProgressDrawTarget {
23 kind: TargetKind,
24}
25
26impl 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)]
227enum 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
247impl 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
267pub(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
286impl<'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
344pub(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
351pub(crate) struct DrawStateWrapper<'a> {
352 state: &'a mut DrawState,
353 orphan_lines: Option<&'a mut Vec<String>>,
354}
355
356impl<'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
372impl std::ops::Deref for DrawStateWrapper<'_> {
373 type Target = DrawState;
374
375 fn deref(&self) -> &Self::Target {
376 self.state
377 }
378}
379
380impl std::ops::DerefMut for DrawStateWrapper<'_> {
381 fn deref_mut(&mut self) -> &mut Self::Target {
382 self.state
383 }
384}
385
386impl 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)]
396struct RateLimiter {
397 interval: u16, // in milliseconds
398 capacity: u8,
399 prev: Instant,
400}
401
402/// Rate limit but allow occasional bursts above desired rate
403impl 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
445const MAX_BURST: u8 = 20;
446
447/// The drawn state of an element.
448#[derive(Clone, Debug, Default)]
449pub(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
460impl 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)]
545mod 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