indicatif/
draw_target.rs

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