Skip to main content

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
10#[cfg(feature = "unicode-width")]
11use console::AnsiCodeIterator;
12use console::{is_dumb, Term, TermTarget};
13#[cfg(feature = "unicode-width")]
14use unicode_width::UnicodeWidthChar;
15#[cfg(all(target_arch = "wasm32", feature = "wasmbind"))]
16use web_time::Instant;
17
18use crate::multi::{MultiProgressAlignment, MultiState};
19use crate::TermLike;
20
21/// Target for draw operations
22///
23/// This tells a [`ProgressBar`](crate::ProgressBar) or a
24/// [`MultiProgress`](crate::MultiProgress) object where to paint to.
25/// The draw target is a stateful wrapper over a drawing destination and
26/// internally optimizes how often the state is painted to the output
27/// device.
28#[derive(Debug)]
29pub struct ProgressDrawTarget {
30    kind: TargetKind,
31}
32
33impl ProgressDrawTarget {
34    /// Draw to a buffered stdout terminal at a max of 20 times a second.
35    ///
36    /// For more information see [`ProgressDrawTarget::term`].
37    pub fn stdout() -> Self {
38        Self::term(Term::buffered_stdout(), 20)
39    }
40
41    /// Draw to a buffered stderr terminal at a max of 20 times a second.
42    ///
43    /// This is the default draw target for progress bars.  For more
44    /// information see [`ProgressDrawTarget::term`].
45    pub fn stderr() -> Self {
46        Self::term(Term::buffered_stderr(), 20)
47    }
48
49    /// Draw to a buffered stdout terminal at a max of `refresh_rate` times a second.
50    ///
51    /// For more information see [`ProgressDrawTarget::term`].
52    pub fn stdout_with_hz(refresh_rate: u8) -> Self {
53        Self::term(Term::buffered_stdout(), refresh_rate)
54    }
55
56    /// Draw to a buffered stderr terminal at a max of `refresh_rate` times a second.
57    ///
58    /// For more information see [`ProgressDrawTarget::term`].
59    pub fn stderr_with_hz(refresh_rate: u8) -> Self {
60        Self::term(Term::buffered_stderr(), refresh_rate)
61    }
62
63    pub(crate) fn new_remote(state: Arc<RwLock<MultiState>>, idx: usize) -> Self {
64        Self {
65            kind: TargetKind::Multi { state, idx },
66        }
67    }
68
69    /// Draw to a terminal, with a specific refresh rate.
70    ///
71    /// Progress bars are by default drawn to terminals however if the
72    /// terminal is not user attended the entire progress bar will be
73    /// hidden.  This is done so that piping to a file will not produce
74    /// useless escape codes in that file.
75    ///
76    /// Progress bars will also be hidden if `TERM` is unset/`dumb`.
77    ///
78    /// Will panic if `refresh_rate` is `0`.
79    pub fn term(term: Term, refresh_rate: u8) -> Self {
80        if !term.is_term() || is_dumb() {
81            return Self::hidden();
82        }
83        Self {
84            kind: TargetKind::Term {
85                term,
86                last_line_count: VisualLines::default(),
87                rate_limiter: RateLimiter::new(refresh_rate),
88                draw_state: DrawState::default(),
89            },
90        }
91    }
92
93    /// Draw to a boxed object that implements the [`TermLike`] trait.
94    ///
95    /// Warning: unlike `stdout()`, `stderr()` and `term()`, this method does not set a default
96    /// refresh rate. For most uses, consider using `term_like_with_hz()` instead.
97    ///
98    /// (indicatif defaults to a refresh rate of 20 times per second in other methods.)
99    pub fn term_like(term_like: Box<dyn TermLike>) -> Self {
100        Self {
101            kind: TargetKind::TermLike {
102                inner: term_like,
103                last_line_count: VisualLines::default(),
104                rate_limiter: None,
105                draw_state: DrawState::default(),
106            },
107        }
108    }
109
110    /// Draw to a boxed object that implements the [`TermLike`] trait,
111    /// with a specific refresh rate.
112    pub fn term_like_with_hz(term_like: Box<dyn TermLike>, refresh_rate: u8) -> Self {
113        Self {
114            kind: TargetKind::TermLike {
115                inner: term_like,
116                last_line_count: VisualLines::default(),
117                rate_limiter: Option::from(RateLimiter::new(refresh_rate)),
118                draw_state: DrawState::default(),
119            },
120        }
121    }
122
123    /// A hidden draw target.
124    ///
125    /// This forces a progress bar to be not rendered at all.
126    pub fn hidden() -> Self {
127        Self {
128            kind: TargetKind::Hidden,
129        }
130    }
131
132    /// Returns true if the draw target is hidden.
133    ///
134    /// This is internally used in progress bars to figure out if overhead
135    /// from drawing can be prevented.
136    pub fn is_hidden(&self) -> bool {
137        match self.kind {
138            TargetKind::Hidden => true,
139            TargetKind::Term { ref term, .. } => !term.is_term(),
140            TargetKind::Multi { ref state, .. } => state.read().unwrap().draw_target.is_hidden(),
141            _ => false,
142        }
143    }
144
145    /// This is used in progress bars to determine whether to use stdout or stderr
146    /// for detecting color support.
147    pub(crate) fn is_stderr(&self) -> bool {
148        match &self.kind {
149            TargetKind::Term { term, .. } => matches!(term.target(), TermTarget::Stderr),
150            TargetKind::Multi { state, .. } => state.read().unwrap().draw_target.is_stderr(),
151            _ => false,
152        }
153    }
154
155    /// Returns the current width of the draw target.
156    pub(crate) fn width(&self) -> Option<u16> {
157        match self.kind {
158            TargetKind::Term { ref term, .. } => Some(term.size().1),
159            TargetKind::Multi { ref state, .. } => state.read().unwrap().draw_target.width(),
160            TargetKind::TermLike { ref inner, .. } => Some(inner.width()),
161            TargetKind::Hidden => None,
162        }
163    }
164
165    /// Notifies the backing `MultiProgress` (if applicable) that the associated progress bar should
166    /// be marked a zombie.
167    pub(crate) fn mark_zombie(&self) {
168        if let TargetKind::Multi { idx, state } = &self.kind {
169            state.write().unwrap().mark_zombie(*idx);
170        }
171    }
172
173    /// Set whether or not to just move cursor instead of clearing lines
174    pub(crate) fn set_move_cursor(&mut self, move_cursor: bool) {
175        match &mut self.kind {
176            TargetKind::Term { draw_state, .. } => draw_state.move_cursor = move_cursor,
177            TargetKind::TermLike { draw_state, .. } => draw_state.move_cursor = move_cursor,
178            _ => {}
179        }
180    }
181
182    /// Apply the given draw state (draws it).
183    pub(crate) fn drawable(&mut self, force_draw: bool, now: Instant) -> Option<Drawable<'_>> {
184        match &mut self.kind {
185            TargetKind::Term {
186                term,
187                last_line_count,
188                rate_limiter,
189                draw_state,
190            } => {
191                match force_draw || rate_limiter.allow(now) {
192                    true => Some(Drawable::Term {
193                        term,
194                        last_line_count,
195                        draw_state,
196                    }),
197                    false => None, // rate limited
198                }
199            }
200            TargetKind::Multi { idx, state, .. } => {
201                let state = state.write().unwrap();
202                Some(Drawable::Multi {
203                    idx: *idx,
204                    state,
205                    force_draw,
206                    now,
207                })
208            }
209            TargetKind::TermLike {
210                inner,
211                last_line_count,
212                rate_limiter,
213                draw_state,
214            } => match force_draw || rate_limiter.as_mut().is_none_or(|r| r.allow(now)) {
215                true => Some(Drawable::TermLike {
216                    term_like: &**inner,
217                    last_line_count,
218                    draw_state,
219                }),
220                false => None, // rate limited
221            },
222            // Hidden, finished, or no need to refresh yet
223            _ => None,
224        }
225    }
226
227    /// Properly disconnects from the draw target
228    pub(crate) fn disconnect(&self, now: Instant) {
229        match self.kind {
230            TargetKind::Term { .. } => {}
231            TargetKind::Multi { idx, ref state, .. } => {
232                let state = state.write().unwrap();
233                let _ = Drawable::Multi {
234                    state,
235                    idx,
236                    force_draw: true,
237                    now,
238                }
239                .clear();
240            }
241            TargetKind::Hidden => {}
242            TargetKind::TermLike { .. } => {}
243        };
244    }
245
246    pub(crate) fn remote(&self) -> Option<(&Arc<RwLock<MultiState>>, usize)> {
247        match &self.kind {
248            TargetKind::Multi { state, idx } => Some((state, *idx)),
249            _ => None,
250        }
251    }
252
253    pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
254        self.kind.adjust_last_line_count(adjust);
255    }
256}
257
258#[derive(Debug)]
259enum TargetKind {
260    Term {
261        term: Term,
262        last_line_count: VisualLines,
263        rate_limiter: RateLimiter,
264        draw_state: DrawState,
265    },
266    Multi {
267        state: Arc<RwLock<MultiState>>,
268        idx: usize,
269    },
270    Hidden,
271    TermLike {
272        inner: Box<dyn TermLike>,
273        last_line_count: VisualLines,
274        rate_limiter: Option<RateLimiter>,
275        draw_state: DrawState,
276    },
277}
278
279impl TargetKind {
280    /// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines
281    fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
282        let last_line_count = match self {
283            Self::Term {
284                last_line_count, ..
285            } => last_line_count,
286            Self::TermLike {
287                last_line_count, ..
288            } => last_line_count,
289            _ => return,
290        };
291
292        match adjust {
293            LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
294            LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
295        }
296    }
297}
298
299pub(crate) enum Drawable<'a> {
300    Term {
301        term: &'a Term,
302        last_line_count: &'a mut VisualLines,
303        draw_state: &'a mut DrawState,
304    },
305    Multi {
306        state: RwLockWriteGuard<'a, MultiState>,
307        idx: usize,
308        force_draw: bool,
309        now: Instant,
310    },
311    TermLike {
312        term_like: &'a dyn TermLike,
313        last_line_count: &'a mut VisualLines,
314        draw_state: &'a mut DrawState,
315    },
316}
317
318impl Drawable<'_> {
319    /// Adjust `last_line_count` such that the next draw operation keeps/clears additional lines
320    pub(crate) fn adjust_last_line_count(&mut self, adjust: LineAdjust) {
321        let last_line_count: &mut VisualLines = match self {
322            Drawable::Term {
323                last_line_count, ..
324            } => last_line_count,
325            Drawable::TermLike {
326                last_line_count, ..
327            } => last_line_count,
328            _ => return,
329        };
330
331        match adjust {
332            LineAdjust::Clear(count) => *last_line_count = last_line_count.saturating_add(count),
333            LineAdjust::Keep(count) => *last_line_count = last_line_count.saturating_sub(count),
334        }
335    }
336
337    pub(crate) fn state(&mut self) -> DrawStateWrapper<'_> {
338        let mut state = match self {
339            Drawable::Term { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
340            Drawable::Multi { state, idx, .. } => state.draw_state(*idx),
341            Drawable::TermLike { draw_state, .. } => DrawStateWrapper::for_term(draw_state),
342        };
343
344        state.reset();
345        state
346    }
347
348    pub(crate) fn clear(mut self) -> io::Result<()> {
349        let state = self.state();
350        drop(state);
351        self.draw()
352    }
353
354    pub(crate) fn draw(self) -> io::Result<()> {
355        match self {
356            Drawable::Term {
357                term,
358                last_line_count,
359                draw_state,
360            } => draw_state.draw_to_term(term, last_line_count),
361            Drawable::Multi {
362                mut state,
363                force_draw,
364                now,
365                ..
366            } => state.draw(force_draw, None, now),
367            Drawable::TermLike {
368                term_like,
369                last_line_count,
370                draw_state,
371            } => draw_state.draw_to_term(term_like, last_line_count),
372        }
373    }
374
375    pub(crate) fn width(&self) -> Option<u16> {
376        match self {
377            Self::Term { term, .. } => Some(term.size().1),
378            Self::Multi { state, .. } => state.draw_target.width(),
379            Self::TermLike { term_like, .. } => Some(term_like.width()),
380        }
381    }
382}
383
384pub(crate) enum LineAdjust {
385    /// Adds to `last_line_count` so that the next draw also clears those lines
386    Clear(VisualLines),
387    /// Subtracts from `last_line_count` so that the next draw retains those lines
388    Keep(VisualLines),
389}
390
391pub(crate) struct DrawStateWrapper<'a> {
392    state: &'a mut DrawState,
393    orphan_lines: Option<&'a mut Vec<LineType>>,
394}
395
396impl<'a> DrawStateWrapper<'a> {
397    pub(crate) fn for_term(state: &'a mut DrawState) -> Self {
398        Self {
399            state,
400            orphan_lines: None,
401        }
402    }
403
404    pub(crate) fn for_multi(state: &'a mut DrawState, orphan_lines: &'a mut Vec<LineType>) -> Self {
405        Self {
406            state,
407            orphan_lines: Some(orphan_lines),
408        }
409    }
410}
411
412impl std::ops::Deref for DrawStateWrapper<'_> {
413    type Target = DrawState;
414
415    fn deref(&self) -> &Self::Target {
416        self.state
417    }
418}
419
420impl std::ops::DerefMut for DrawStateWrapper<'_> {
421    fn deref_mut(&mut self) -> &mut Self::Target {
422        self.state
423    }
424}
425
426impl Drop for DrawStateWrapper<'_> {
427    fn drop(&mut self) {
428        if let Some(text_lines) = &mut self.orphan_lines {
429            // Filter out the lines that do not contain progress information
430            // Store the filtered out lines in orphaned
431            let mut lines = Vec::new();
432
433            for line in self.state.lines.drain(..) {
434                match &line {
435                    LineType::Text(_) | LineType::Empty => text_lines.push(line),
436                    _ => lines.push(line),
437                }
438            }
439
440            self.state.lines = lines;
441        }
442    }
443}
444
445#[derive(Debug)]
446struct RateLimiter {
447    interval: u16, // in milliseconds
448    capacity: u8,
449    prev: Instant,
450}
451
452/// Rate limit but allow occasional bursts above desired rate
453impl RateLimiter {
454    fn new(rate: u8) -> Self {
455        Self {
456            interval: 1000 / (rate as u16), // between 3 and 1000 milliseconds
457            capacity: MAX_BURST,
458            prev: Instant::now(),
459        }
460    }
461
462    fn allow(&mut self, now: Instant) -> bool {
463        if now < self.prev {
464            return false;
465        }
466
467        let elapsed = now - self.prev;
468        // If `capacity` is 0 and not enough time (`self.interval` ms) has passed since
469        // `self.prev` to add new capacity, return `false`. The goal of this method is to
470        // make this decision as efficient as possible.
471        if self.capacity == 0 && elapsed < Duration::from_millis(self.interval as u64) {
472            return false;
473        }
474
475        // We now calculate `new`, the number of ms, since we last returned `true`,
476        // and `remainder`, which represents a number of ns less than 1ms which we cannot
477        // convert into capacity now, so we're saving it for later.
478        let (new, remainder) = (
479            elapsed.as_millis() / self.interval as u128,
480            elapsed.as_nanos() % (self.interval as u128 * 1_000_000),
481        );
482
483        // We add `new` to `capacity`, subtract one for returning `true` from here,
484        // then make sure it does not exceed a maximum of `MAX_BURST`, then store it.
485        self.capacity = Ord::min(MAX_BURST as u128, (self.capacity as u128) + new - 1) as u8;
486        // Store `prev` for the next iteration after subtracting the `remainder`.
487        // Just use `unwrap` here because it shouldn't be possible for this to underflow.
488        self.prev = now
489            .checked_sub(Duration::from_nanos(remainder as u64))
490            .unwrap();
491        true
492    }
493}
494
495const MAX_BURST: u8 = 20;
496
497/// The drawn state of an element.
498#[derive(Clone, Debug, Default)]
499pub(crate) struct DrawState {
500    /// The lines to print (can contain ANSI codes)
501    pub(crate) lines: Vec<LineType>,
502    /// True if we should move the cursor up when possible instead of clearing lines.
503    pub(crate) move_cursor: bool,
504    /// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
505    pub(crate) alignment: MultiProgressAlignment,
506}
507
508impl DrawState {
509    /// Draw the current state to the terminal
510    /// We expect a few things:
511    /// - self.lines contains n lines of text/empty then m lines of bars
512    /// - None of those lines contain newlines
513    fn draw_to_term(
514        &mut self,
515        term: &(impl TermLike + ?Sized),
516        bar_count: &mut VisualLines, // The number of dynamic lines printed at the previous tick
517    ) -> io::Result<()> {
518        if panicking() {
519            return Ok(());
520        }
521
522        if !self.lines.is_empty() && self.move_cursor {
523            // Move up to first line (assuming the last line doesn't contain a '\n') and then move to then front of the line
524            term.move_cursor_up(bar_count.as_usize().saturating_sub(1))?;
525            term.write_str("\r")?;
526        } else {
527            // Fork of console::clear_last_lines that assumes that the last line doesn't contain a '\n'
528            let n = bar_count.as_usize();
529            term.move_cursor_up(n.saturating_sub(1))?;
530            for i in 0..n {
531                term.clear_line()?;
532                if i + 1 != n {
533                    term.move_cursor_down(1)?;
534                }
535            }
536            term.move_cursor_up(n.saturating_sub(1))?;
537        }
538
539        let term_width = term.width() as usize;
540
541        // Here we calculate the terminal vertical real estate that the state requires
542        let full_height = self.visual_line_count(.., term_width);
543
544        let shift = match self.alignment {
545            // If we align to the bottom and the new height is less than before, clear the lines
546            // that are not used by the new content.
547            MultiProgressAlignment::Bottom if full_height < *bar_count => {
548                let shift = *bar_count - full_height;
549                for _ in 0..shift.as_usize() {
550                    term.write_line("")?;
551                }
552                shift
553            }
554            _ => VisualLines::default(),
555        };
556
557        // Accumulate the displayed height in here. This differs from `full_height` in that it will
558        // accurately reflect the number of lines that have been displayed on the terminal, if the
559        // full height exceeds the terminal height.
560        let mut real_height = VisualLines::default();
561
562        for line in self.lines.iter() {
563            let metrics = line.wrapped_metrics(term_width);
564
565            // Check here for bar lines that exceed the terminal height
566            if matches!(line, LineType::Bar(_)) {
567                // Stop here if printing this bar would exceed the terminal height
568                if real_height + metrics.height > term.height().into() {
569                    break;
570                }
571
572                real_height += metrics.height;
573            }
574
575            term.write_str(line.as_ref())?;
576
577            // clear the line and keep the cursor on the right terminal side so that
578            // future writes/prints will happen on the next line
579            let line_filler = term_width - metrics.last_line_width;
580            term.write_str(&" ".repeat(line_filler))?;
581        }
582
583        term.flush()?;
584        *bar_count = real_height + shift;
585
586        Ok(())
587    }
588
589    fn reset(&mut self) {
590        self.lines.clear();
591    }
592
593    pub(crate) fn visual_line_count(
594        &self,
595        range: impl SliceIndex<[LineType], Output = [LineType]>,
596        width: usize,
597    ) -> VisualLines {
598        visual_line_count(&self.lines[range], width)
599    }
600}
601
602#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
603pub(crate) struct VisualLines(usize);
604
605impl VisualLines {
606    pub(crate) fn saturating_add(&self, other: Self) -> Self {
607        Self(self.0.saturating_add(other.0))
608    }
609
610    pub(crate) fn saturating_sub(&self, other: Self) -> Self {
611        Self(self.0.saturating_sub(other.0))
612    }
613
614    pub(crate) fn as_usize(&self) -> usize {
615        self.0
616    }
617}
618
619impl Add for VisualLines {
620    type Output = Self;
621
622    fn add(self, rhs: Self) -> Self::Output {
623        Self(self.0 + rhs.0)
624    }
625}
626
627impl AddAssign for VisualLines {
628    fn add_assign(&mut self, rhs: Self) {
629        self.0 += rhs.0;
630    }
631}
632
633impl<T: Into<usize>> From<T> for VisualLines {
634    fn from(value: T) -> Self {
635        Self(value.into())
636    }
637}
638
639impl Sub for VisualLines {
640    type Output = Self;
641
642    fn sub(self, rhs: Self) -> Self::Output {
643        Self(self.0 - rhs.0)
644    }
645}
646
647/// Calculate the number of visual lines in the given lines, after
648/// accounting for line wrapping and non-printable characters.
649pub(crate) fn visual_line_count(lines: &[LineType], width: usize) -> VisualLines {
650    lines.iter().fold(VisualLines::default(), |acc, line| {
651        acc.saturating_add(line.wrapped_height(width))
652    })
653}
654
655#[derive(Clone, Debug)]
656pub(crate) enum LineType {
657    Text(String),
658    Bar(String),
659    Empty,
660}
661
662impl LineType {
663    fn wrapped_height(&self, width: usize) -> VisualLines {
664        self.wrapped_metrics(width).height
665    }
666
667    #[cfg(feature = "unicode-width")]
668    fn wrapped_metrics(&self, width: usize) -> Metrics {
669        // When a wide character such as CJK appears at the end of wrap with
670        // only 1 column available, the line wraps before the character, leaving
671        // an empty column.
672        // The `effective_width` takes such empty columns into account.
673        let str = self.as_ref();
674        let mut num_lines: usize = 1;
675        let mut column: usize = 0;
676        for (substr, is_ansi) in AnsiCodeIterator::new(str) {
677            if is_ansi {
678                continue;
679            }
680            for ch in substr.chars() {
681                let Some(ch_width) = UnicodeWidthChar::width(ch) else {
682                    continue; // Skip control characters.
683                };
684                column += ch_width;
685                if column > width {
686                    num_lines += 1;
687                    column = ch_width;
688                }
689            }
690        }
691        Metrics {
692            height: num_lines.into(),
693            last_line_width: column,
694        }
695    }
696
697    #[cfg(not(feature = "unicode-width"))]
698    fn wrapped_metrics(&self, width: usize) -> Metrics {
699        // Calculate real length based on terminal width
700        // This take in account linewrap from terminal
701        let unwrapped_width = self.console_width();
702        let terminal_len = (unwrapped_width as f64 / width as f64).ceil() as usize;
703
704        // If the line is effectively empty (for example when it consists
705        // solely of ANSI color code sequences, count it the same as a
706        // new line. If the line is measured to be len = 0, we will
707        // subtract with overflow later.
708        let height = usize::max(terminal_len, 1);
709        Metrics {
710            height: height.into(),
711            last_line_width: unwrapped_width - width * (height - 1),
712        }
713    }
714
715    #[cfg(not(feature = "unicode-width"))]
716    fn console_width(&self) -> usize {
717        console::measure_text_width(self.as_ref())
718    }
719}
720
721impl AsRef<str> for LineType {
722    fn as_ref(&self) -> &str {
723        match self {
724            LineType::Text(s) | LineType::Bar(s) => s,
725            LineType::Empty => "",
726        }
727    }
728}
729
730impl PartialEq<str> for LineType {
731    fn eq(&self, other: &str) -> bool {
732        self.as_ref() == other
733    }
734}
735
736/// Metrics of wrapped lines.
737#[derive(Debug)]
738struct Metrics {
739    /// The number of lines.
740    height: VisualLines,
741    /// The width of the last line.
742    last_line_width: usize,
743}
744
745#[cfg(test)]
746mod tests {
747    use crate::draw_target::{LineType, TargetKind};
748    use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
749    use console::Term;
750
751    #[test]
752    fn multi_is_hidden() {
753        let mp = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
754
755        let pb = mp.add(ProgressBar::new(100));
756        assert!(mp.is_hidden());
757        assert!(pb.is_hidden());
758    }
759
760    #[test]
761    fn real_line_count_test() {
762        #[derive(Debug)]
763        struct Case {
764            lines: &'static [&'static str],
765            expectation: usize,
766            width: usize,
767        }
768
769        let lines_and_expectations = [
770            Case {
771                lines: &["1234567890"],
772                expectation: 1,
773                width: 10,
774            },
775            Case {
776                lines: &["1234567890"],
777                expectation: 2,
778                width: 5,
779            },
780            Case {
781                lines: &["1234567890"],
782                expectation: 3,
783                width: 4,
784            },
785            Case {
786                lines: &["1234567890"],
787                expectation: 4,
788                width: 3,
789            },
790            Case {
791                lines: &["1234567890", "", "1234567890"],
792                expectation: 3,
793                width: 10,
794            },
795            Case {
796                lines: &["1234567890", "", "1234567890"],
797                expectation: 5,
798                width: 5,
799            },
800            Case {
801                lines: &["1234567890", "", "1234567890"],
802                expectation: 7,
803                width: 4,
804            },
805            Case {
806                lines: &["aaaaaaaaaaaaa", "", "bbbbbbbbbbbbbbbbb", "", "ccccccc"],
807                expectation: 8,
808                width: 7,
809            },
810            Case {
811                lines: &["", "", "", "", ""],
812                expectation: 5,
813                width: 6,
814            },
815            Case {
816                // These lines contain only ANSI escape sequences, so they should only count as 1 line
817                lines: &["\u{1b}[1m\u{1b}[1m\u{1b}[1m", "\u{1b}[1m\u{1b}[1m\u{1b}[1m"],
818                expectation: 2,
819                width: 5,
820            },
821            Case {
822                // These lines contain  ANSI escape sequences and two effective chars, so they should only count as 1 line still
823                lines: &[
824                    "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
825                    "a\u{1b}[1m\u{1b}[1m\u{1b}[1ma",
826                ],
827                expectation: 2,
828                width: 5,
829            },
830            Case {
831                // These lines contain ANSI escape sequences and six effective chars, so they should count as 2 lines each
832                lines: &[
833                    "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
834                    "aa\u{1b}[1m\u{1b}[1m\u{1b}[1mabcd",
835                ],
836                expectation: 4,
837                width: 5,
838            },
839        ];
840
841        for case in lines_and_expectations.iter() {
842            let result = super::visual_line_count(
843                &case
844                    .lines
845                    .iter()
846                    .map(|s| LineType::Text(s.to_string()))
847                    .collect::<Vec<_>>(),
848                case.width,
849            );
850            assert_eq!(result, case.expectation.into(), "case: {case:?}");
851        }
852    }
853
854    #[test]
855    fn is_stderr_for_multi() {
856        let term = Term::buffered_stderr();
857        let draw_target = ProgressDrawTarget {
858            kind: TargetKind::Term {
859                term,
860                last_line_count: Default::default(),
861                rate_limiter: super::RateLimiter::new(20),
862                draw_state: Default::default(),
863            },
864        };
865        assert!(draw_target.is_stderr());
866
867        let mp = MultiProgress::with_draw_target(draw_target);
868        let multi_draw_target = ProgressDrawTarget {
869            kind: TargetKind::Multi {
870                state: mp.state.clone(),
871                idx: 0,
872            },
873        };
874        assert!(multi_draw_target.is_stderr());
875    }
876
877    #[test]
878    fn wrapped_height_cjk_at_the_end_wrap() {
879        // Although the text is 20 columns (18 ASCII and 1 wide), when the width
880        // is 10, its height should be 3 because the wide character can't be
881        // broken in the middle.
882        let text = "123456789国123456789";
883        let line_type = LineType::Text(text.to_string());
884        let metrics = line_type.wrapped_metrics(10);
885        #[cfg(feature = "unicode-width")]
886        {
887            assert_eq!(metrics.height.as_usize(), 3);
888            assert_eq!(metrics.last_line_width, 1);
889        }
890        #[cfg(not(feature = "unicode-width"))]
891        {
892            assert_eq!(metrics.height.as_usize(), 2);
893            assert_eq!(metrics.last_line_width, 9);
894        }
895    }
896}