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