Skip to main content

rich/
segment.rs

1//! Segments — the atoms of rendering.
2//!
3//! Port of upstream `rich/segment.py`. A [`Segment`] is a piece of text with an
4//! optional [`Style`]. Everything renderable ultimately becomes a stream of
5//! segments, which the [`Console`](crate::console::Console) turns into bytes.
6//!
7//! Control-code segments carry a `control` flag; the typed control sequences
8//! that populate them live in [`control`](crate::control).
9
10use crate::cells::cell_len;
11use crate::style::Style;
12
13/// A span of text with an optional style. Mirrors `rich.segment.Segment`.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Segment {
16    /// The text: a [`CompactString`](crate::CompactString), kept inline up
17    /// to 24 bytes, so a short run allocates nothing. It reads as a `&str`.
18    pub text: crate::CompactString,
19    pub style: Option<Style>,
20    /// Whether this segment carries terminal control codes rather than content.
21    pub control: bool,
22}
23
24impl Segment {
25    /// A plain content segment.
26    pub fn new(text: impl Into<crate::CompactString>, style: Option<Style>) -> Self {
27        Segment {
28            text: text.into(),
29            style,
30            control: false,
31        }
32    }
33
34    /// A newline segment (`Segment.line()` upstream).
35    pub fn line() -> Self {
36        Segment {
37            text: crate::CompactString::const_new("\n"),
38            style: None,
39            control: false,
40        }
41    }
42
43    /// A control segment (carries no visible width).
44    pub fn control(text: impl Into<crate::CompactString>) -> Self {
45        Segment {
46            text: text.into(),
47            style: None,
48            control: true,
49        }
50    }
51
52    /// The number of terminal cells this segment occupies (0 for control).
53    pub fn cell_length(&self) -> usize {
54        if self.control {
55            0
56        } else {
57            cell_len(&self.text)
58        }
59    }
60
61    /// Merge adjacent segments that share the same style and control flag.
62    /// Port of `Segment.simplify`.
63    pub fn simplify(segments: &[Segment]) -> Vec<Segment> {
64        let mut out: Vec<Segment> = Vec::with_capacity(segments.len());
65        for segment in segments {
66            match out.last_mut() {
67                Some(last) if last.style == segment.style && last.control == segment.control => {
68                    last.text.push_str(&segment.text);
69                }
70                _ => out.push(segment.clone()),
71            }
72        }
73        out
74    }
75
76    /// Remove all colour from a segment stream, keeping every other attribute.
77    /// Port of `Segment.remove_color`.
78    pub fn remove_color(segments: &[Segment]) -> Vec<Segment> {
79        segments
80            .iter()
81            .map(|segment| Segment {
82                text: segment.text.clone(),
83                style: segment.style.as_ref().map(Style::without_color),
84                control: segment.control,
85            })
86            .collect()
87    }
88
89    /// Apply `style` as a base *under* each segment's own style (that segment's
90    /// style wins on top). Control segments are left untouched. Port of
91    /// `Segment.apply_style` (the `style`-only path).
92    ///
93    /// Line-break segments (`"\n"`) are also left unstyled: upstream's
94    /// line-oriented print pipeline re-emits row separators plain, so styling
95    /// them would add stray SGR runs around every newline.
96    pub fn apply_style(segments: &[Segment], style: &Style) -> Vec<Segment> {
97        segments
98            .iter()
99            .map(|segment| {
100                if segment.control || segment.text == "\n" {
101                    segment.clone()
102                } else {
103                    let combined = match &segment.style {
104                        Some(own) => style.combine(own),
105                        None => style.clone(),
106                    };
107                    Segment {
108                        text: segment.text.clone(),
109                        style: Some(combined),
110                        control: false,
111                    }
112                }
113            })
114            .collect()
115    }
116
117    /// Split a flat segment stream into lines, breaking on `\n`.
118    ///
119    /// Port of `Segment.split_lines`. Newline characters are consumed (not kept
120    /// in the output); a trailing newline yields a final empty line only if
121    /// there was content after the last break — or an explicit empty segment,
122    /// which is how a renderable whose last line is empty says so (the
123    /// port's streams separate lines rather than end them).
124    pub fn split_lines(segments: &[Segment]) -> Vec<Vec<Segment>> {
125        let mut lines: Vec<Vec<Segment>> = Vec::new();
126        let mut current: Vec<Segment> = Vec::new();
127        // An empty segment right after a break: the final line is empty.
128        let mut empty_last_line = false;
129        for segment in segments {
130            if segment.control || !segment.text.contains('\n') {
131                if !segment.text.is_empty() {
132                    current.push(segment.clone());
133                } else if !segment.control && current.is_empty() && !lines.is_empty() {
134                    empty_last_line = true;
135                }
136                continue;
137            }
138            empty_last_line = false;
139            let mut parts = segment.text.split('\n').peekable();
140            while let Some(part) = parts.next() {
141                if !part.is_empty() {
142                    current.push(Segment::new(part, segment.style.clone()));
143                }
144                if parts.peek().is_some() {
145                    // The break between parts closes the current line.
146                    lines.push(std::mem::take(&mut current));
147                }
148            }
149        }
150        if !current.is_empty() || empty_last_line {
151            lines.push(current);
152        }
153        lines
154    }
155
156    /// [`split_lines`](Self::split_lines) of segments the caller is done
157    /// with: each is moved into its line rather than cloned, which a render
158    /// does once for every segment it makes (docs/PORTING.md, "Performance
159    /// notes"). `split_lines_owned_matches_split_lines` holds the two together.
160    pub(crate) fn split_lines_owned(segments: Vec<Segment>) -> Vec<Vec<Segment>> {
161        let mut lines: Vec<Vec<Segment>> = Vec::new();
162        let mut current: Vec<Segment> = Vec::new();
163        let mut empty_last_line = false;
164        for segment in segments {
165            if segment.control || !segment.text.contains('\n') {
166                if !segment.text.is_empty() {
167                    current.push(segment);
168                } else if !segment.control && current.is_empty() && !lines.is_empty() {
169                    empty_last_line = true;
170                }
171                continue;
172            }
173            empty_last_line = false;
174            let mut parts = segment.text.split('\n').peekable();
175            while let Some(part) = parts.next() {
176                if !part.is_empty() {
177                    current.push(Segment::new(part, segment.style.clone()));
178                }
179                if parts.peek().is_some() {
180                    lines.push(std::mem::take(&mut current));
181                }
182            }
183        }
184        if !current.is_empty() || empty_last_line {
185            lines.push(current);
186        }
187        lines
188    }
189
190    /// Shape a set of lines into exactly `height` rows of `width` cells: crop
191    /// extra rows, pad each row to `width`, and append blank rows to reach
192    /// `height`. Port of `Segment.set_shape` (`style=None`, `new_lines=False`).
193    pub fn set_shape(lines: Vec<Vec<Segment>>, width: usize, height: usize) -> Vec<Vec<Segment>> {
194        let mut shaped: Vec<Vec<Segment>> = lines
195            .into_iter()
196            .take(height)
197            .map(|line| Segment::adjust_line_length(&line, width, None))
198            .collect();
199        while shaped.len() < height {
200            shaped.push(vec![Segment::new(" ".repeat(width), None)]);
201        }
202        shaped
203    }
204
205    /// Fold every line to at most `width` cells, breaking at **word boundaries**
206    /// the way upstream's word wrapping does.
207    ///
208    /// [`fold_lines`](Self::fold_lines) breaks wherever the row happens to fill
209    /// up, which splits identifiers and words mid-character-run
210    /// (`epsilon, z` / `eta, eta, theta)`). Upstream's `word_wrap=True` routes
211    /// through `_wrap.divide_line`, which we already port for `Text` — this
212    /// applies the same break offsets to a styled segment run, so styles survive
213    /// the split.
214    ///
215    /// A word longer than `width` is still folded mid-word; there is nowhere
216    /// else to break it.
217    pub fn fold_lines_words(segments: &[Segment], width: usize) -> Vec<Segment> {
218        if width == 0 {
219            return segments.to_vec();
220        }
221        let mut out = Vec::new();
222        let lines = Self::split_lines(segments);
223        let last = lines.len().saturating_sub(1);
224        for (index, line) in lines.into_iter().enumerate() {
225            let plain: String = line
226                .iter()
227                .filter(|segment| !segment.control)
228                .map(|segment| segment.text.as_str())
229                .collect();
230            let breaks = crate::wrap::divide_line(&plain, width, true);
231
232            let mut char_pos = 0usize;
233            let mut next_break = 0usize;
234            for segment in line {
235                if segment.control {
236                    out.push(segment);
237                    continue;
238                }
239                let mut buf = String::new();
240                for ch in segment.text.chars() {
241                    while next_break < breaks.len() && char_pos == breaks[next_break] {
242                        if !buf.is_empty() {
243                            out.push(Segment::new(buf.clone(), segment.style.clone()));
244                            buf.clear();
245                        }
246                        out.push(Segment::line());
247                        next_break += 1;
248                    }
249                    buf.push(ch);
250                    char_pos += 1;
251                }
252                if !buf.is_empty() {
253                    out.push(Segment::new(buf, segment.style.clone()));
254                }
255            }
256            if index != last {
257                out.push(Segment::line());
258            }
259        }
260        out
261    }
262
263    /// Fold every line to at most `width` cells, carrying the overflow onto
264    /// continuation lines instead of discarding it.
265    ///
266    /// [`crop_lines`](Self::crop_lines) is the display backstop and **throws the
267    /// remainder away** — correct for a renderable that has already wrapped
268    /// itself, and data loss for one that emits a long line verbatim. Styles are
269    /// preserved across the split.
270    ///
271    /// This breaks wherever the row happens to fill up, so it splits words. That
272    /// is right only for content upstream folds *without* word wrapping. A
273    /// renderable whose upstream counterpart goes through `Text.wrap` wants
274    /// [`fold_lines_words`](Self::fold_lines_words) instead: reaching for this
275    /// one is what made `--json` print `over t` / `he lazy` where rich prints
276    /// `over ` / `the lazy`.
277    pub fn fold_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
278        if width == 0 {
279            return segments.to_vec();
280        }
281        let mut out = Vec::new();
282        let lines = Self::split_lines(segments);
283        let last = lines.len().saturating_sub(1);
284        for (index, line) in lines.into_iter().enumerate() {
285            let mut used = 0usize;
286            for segment in line {
287                if segment.control {
288                    out.push(segment);
289                    continue;
290                }
291                // Walk the segment in cell-sized pieces, breaking whenever the
292                // current row is full.
293                let mut remaining = segment.text.as_str();
294                while !remaining.is_empty() {
295                    let room = width.saturating_sub(used);
296                    if room == 0 {
297                        out.push(Segment::line());
298                        used = 0;
299                        continue;
300                    }
301                    let chunks = crate::cells::chop_cells(remaining, room);
302                    let mut head = chunks.first().cloned().unwrap_or_default();
303                    if head.is_empty() {
304                        if used > 0 {
305                            // The row has content but no space for this
306                            // character; start a fresh one and try again.
307                            out.push(Segment::line());
308                            used = 0;
309                            continue;
310                        }
311                        // Already at the start of a row and the glyph STILL does
312                        // not fit — a 2-cell glyph at width 1. Emit it anyway,
313                        // overflowing by a cell.
314                        //
315                        // A whole *grapheme*, not a single code point: taking one
316                        // code point off `"❤️"` emits a bare `❤` and leaves a
317                        // stranded variation selector to be emitted on the next
318                        // row, where it silently re-widens whatever character
319                        // precedes it.
320                        //
321                        // Retrying here instead was an infinite loop that
322                        // allocated a line break per iteration: ~400 MB/s until
323                        // the process was killed. Every branch of this loop must
324                        // consume input.
325                        let (spans, _) = crate::cells::split_graphemes(remaining);
326                        let take = spans.first().map_or(remaining.len(), |span| span.1);
327                        head = remaining[..take].to_string();
328                    }
329                    used += crate::cells::cell_len(&head);
330                    remaining = &remaining[head.len()..];
331                    out.push(Segment::new(head, segment.style.clone()));
332                    if !remaining.is_empty() {
333                        out.push(Segment::line());
334                        used = 0;
335                    }
336                }
337            }
338            if index != last {
339                out.push(Segment::line());
340            }
341        }
342        out
343    }
344
345    /// Crop every line in a segment stream to at most `width` cells, discarding
346    /// the excess and leaving short lines alone.
347    ///
348    /// Port of `Segment.split_and_crop_lines` with `pad=False`, which is what
349    /// `Console.print(crop=True)` applies to the finished stream. It is the only
350    /// thing standing between an [`Overflow::Ignore`](crate::console::Overflow)
351    /// text and a line that runs off the side of the terminal.
352    ///
353    /// As upstream, any non-control segment containing `\n` is split there —
354    /// a print `end` such as `"!!\n"` arrives as one segment, and cropping it
355    /// whole would lose its newline — and each newline is re-emitted as a bare
356    /// [`Segment::line`]. A line is cropped only when it is wider than `width`;
357    /// then everything past the edge goes, zero-width characters and control
358    /// segments included (`adjust_line_length`).
359    pub fn crop_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
360        let mut result: Vec<Segment> = Vec::with_capacity(segments.len());
361        let mut line: Vec<Segment> = Vec::new();
362        for segment in segments {
363            if !segment.control && segment.text.contains('\n') {
364                let mut pieces = segment.text.split('\n').peekable();
365                while let Some(piece) = pieces.next() {
366                    if !piece.is_empty() {
367                        line.push(Segment::new(piece, segment.style.clone()));
368                    }
369                    if pieces.peek().is_some() {
370                        result.extend(Segment::crop_line(&line, width));
371                        result.push(Segment::line());
372                        line.clear();
373                    }
374                }
375            } else {
376                line.push(segment.clone());
377            }
378        }
379        if !line.is_empty() {
380            result.extend(Segment::crop_line(&line, width));
381        }
382        result
383    }
384
385    /// The crop half of `Segment.adjust_line_length`: a line no wider than
386    /// `length` is returned whole; a wider one keeps segments while they end
387    /// strictly inside `length` (control segments always), cuts the first one
388    /// that reaches the edge with `set_cell_size`, and drops the rest.
389    fn crop_line(line: &[Segment], length: usize) -> Vec<Segment> {
390        let line_length: usize = line.iter().map(Segment::cell_length).sum();
391        if line_length <= length {
392            return line.to_vec();
393        }
394        let mut new_line: Vec<Segment> = Vec::new();
395        let mut used = 0usize;
396        for segment in line {
397            let segment_length = segment.cell_length();
398            if used + segment_length < length || segment.control {
399                new_line.push(segment.clone());
400                used += segment_length;
401            } else {
402                let cropped = crate::cells::set_cell_size(&segment.text, length - used);
403                new_line.push(Segment::new(cropped, segment.style.clone()));
404                break;
405            }
406        }
407        new_line
408    }
409
410    /// Pad (with a styled space run) or crop a single line to exactly `length`
411    /// cells. Port of `Segment.adjust_line_length`.
412    pub fn adjust_line_length(
413        line: &[Segment],
414        length: usize,
415        style: Option<Style>,
416    ) -> Vec<Segment> {
417        let line_length: usize = line.iter().map(Segment::cell_length).sum();
418        if line_length < length {
419            let mut new_line = line.to_vec();
420            new_line.push(Segment::new(" ".repeat(length - line_length), style));
421            new_line
422        } else {
423            Segment::crop_line(line, length)
424        }
425    }
426}
427
428impl Segment {
429    /// Whether the segment carries control codes. Port of
430    /// `Segment.is_control`.
431    pub fn is_control(&self) -> bool {
432        self.control
433    }
434
435    /// Split into two segments at cell `cut`. A double-width character
436    /// that straddles the cut becomes a space on each side. Port of
437    /// `Segment.split_cells`.
438    pub fn split_cells(&self, cut: usize) -> (Segment, Segment) {
439        let with_text = |text: String| Segment {
440            text: text.into(),
441            style: self.style.clone(),
442            control: self.control,
443        };
444        let chars: Vec<char> = self.text.chars().collect();
445        if chars.iter().all(|&c| crate::cells::char_cell_width(c) == 1) {
446            if cut >= chars.len() {
447                return (self.clone(), with_text(String::new()));
448            }
449            return (
450                with_text(chars[..cut].iter().collect()),
451                with_text(chars[cut..].iter().collect()),
452            );
453        }
454        let cell_length = self.cell_length();
455        if cut >= cell_length {
456            return (self.clone(), with_text(String::new()));
457        }
458        let size = |c: char| crate::cells::char_cell_width(c);
459        let text = |range: std::ops::Range<usize>| -> String { chars[range].iter().collect() };
460        let mut pos = ((cut as f64 / cell_length as f64) * chars.len() as f64) as usize;
461        // Upstream searches without a bound, and loops forever when the cut
462        // falls inside a multi-character grapheme; this gives up after one
463        // pass over the text and splits as `cells.split_text` does.
464        for _ in 0..=chars.len() * 2 + 2 {
465            let cell_pos = crate::cells::cell_len(&text(0..pos));
466            if cell_pos == cut {
467                return (with_text(text(0..pos)), with_text(text(pos..chars.len())));
468            }
469            if cell_pos + 1 == cut && pos < chars.len() && size(chars[pos]) == 2 {
470                return (
471                    with_text(text(0..pos) + " "),
472                    with_text(" ".to_string() + &text(pos + 1..chars.len())),
473                );
474            }
475            if cell_pos == cut + 1 && pos > 0 && size(chars[pos - 1]) == 2 {
476                return (
477                    with_text(text(0..pos - 1) + " "),
478                    with_text(" ".to_string() + &text(pos..chars.len())),
479                );
480            }
481            if cell_pos < cut {
482                pos = (pos + 1).min(chars.len());
483            } else {
484                pos = pos.saturating_sub(1);
485            }
486        }
487        let (before, after) = crate::cells::split_text(&self.text, cut);
488        (with_text(before), with_text(after))
489    }
490
491    /// Apply `style` under each segment's own style and `post_style` over
492    /// it; control segments lose their style. Port of `Segment.apply_style`
493    /// with both arguments ([`apply_style`](Self::apply_style) is the
494    /// port's line-aware form of the `style`-only path).
495    pub fn apply_style_with(
496        segments: &[Segment],
497        style: Option<&Style>,
498        post_style: Option<&Style>,
499    ) -> Vec<Segment> {
500        let style = style.filter(|style| !style.is_null());
501        let post_style = post_style.filter(|style| !style.is_null());
502        segments
503            .iter()
504            .map(|segment| {
505                let mut own = segment.style.clone();
506                if let Some(style) = style {
507                    own = if segment.control {
508                        None
509                    } else {
510                        Some(match &own {
511                            Some(own) => style.combine(own),
512                            None => style.clone(),
513                        })
514                    };
515                }
516                if let Some(post_style) = post_style {
517                    own = if segment.control {
518                        None
519                    } else {
520                        Some(match own.filter(|own| !own.is_null()) {
521                            Some(own) => own.combine(post_style),
522                            None => post_style.clone(),
523                        })
524                    };
525                }
526                Segment {
527                    text: segment.text.clone(),
528                    style: own,
529                    control: segment.control,
530                }
531            })
532            .collect()
533    }
534
535    /// Keep only the control segments (`is_control`) or only the others.
536    /// Port of `Segment.filter_control`.
537    pub fn filter_control(segments: &[Segment], is_control: bool) -> Vec<Segment> {
538        segments
539            .iter()
540            .filter(|segment| segment.control == is_control)
541            .cloned()
542            .collect()
543    }
544
545    /// Split into lines on `\n`, each with whether a newline ended it; a
546    /// last line without one comes only when it holds something. Port of
547    /// `Segment.split_lines_terminator`.
548    pub fn split_lines_terminator(segments: &[Segment]) -> Vec<(Vec<Segment>, bool)> {
549        let mut lines = Vec::new();
550        let mut line: Vec<Segment> = Vec::new();
551        for segment in segments {
552            if segment.text.contains('\n') && !segment.control {
553                let mut pieces = segment.text.split('\n').peekable();
554                while let Some(piece) = pieces.next() {
555                    if !piece.is_empty() {
556                        line.push(Segment::new(piece, segment.style.clone()));
557                    }
558                    if pieces.peek().is_some() {
559                        lines.push((std::mem::take(&mut line), true));
560                    }
561                }
562            } else {
563                line.push(segment.clone());
564            }
565        }
566        if !line.is_empty() {
567            lines.push((line, false));
568        }
569        lines
570    }
571
572    /// Split into lines on `\n` and fit each to `length` cells, padding with
573    /// `style` when `pad` is set and ending each terminated line with a
574    /// newline segment when `include_new_lines` is. Port of
575    /// `Segment.split_and_crop_lines`.
576    pub fn split_and_crop_lines(
577        segments: &[Segment],
578        length: usize,
579        style: Option<&Style>,
580        pad: bool,
581        include_new_lines: bool,
582    ) -> Vec<Vec<Segment>> {
583        Segment::split_lines_terminator(segments)
584            .into_iter()
585            .map(|(line, terminated)| {
586                let mut cropped =
587                    Segment::adjust_line_length_with(&line, length, style.cloned(), pad);
588                if terminated && include_new_lines {
589                    cropped.push(Segment::line());
590                }
591                cropped
592            })
593            .collect()
594    }
595
596    /// [`adjust_line_length`](Self::adjust_line_length) with upstream's `pad`
597    /// argument: a short line is padded only when it is set. Port of
598    /// `Segment.adjust_line_length`.
599    pub fn adjust_line_length_with(
600        line: &[Segment],
601        length: usize,
602        style: Option<Style>,
603        pad: bool,
604    ) -> Vec<Segment> {
605        let line_length: usize = line.iter().map(Segment::cell_length).sum();
606        if line_length < length && !pad {
607            return line.to_vec();
608        }
609        Segment::adjust_line_length(line, length, style)
610    }
611
612    /// The cell length of a line, control segments excluded. Port of
613    /// `Segment.get_line_length`.
614    pub fn get_line_length(line: &[Segment]) -> usize {
615        line.iter()
616            .filter(|segment| !segment.control)
617            .map(|segment| cell_len(&segment.text))
618            .sum()
619    }
620
621    /// The width of the widest line and the number of lines. Port of
622    /// `Segment.get_shape`.
623    pub fn get_shape(lines: &[Vec<Segment>]) -> (usize, usize) {
624        let width = lines
625            .iter()
626            .map(|line| Segment::get_line_length(line))
627            .max()
628            .unwrap_or(0);
629        (width, lines.len())
630    }
631
632    /// Every line adjusted to `width`, then blank lines of `style` added up
633    /// to `height` (the line count when `None`); blank lines end in `\n`
634    /// with `new_lines`. Port of `Segment.set_shape`, which, unlike
635    /// [`set_shape`](Self::set_shape), does not drop lines past `height`.
636    pub fn set_shape_with(
637        lines: &[Vec<Segment>],
638        width: usize,
639        height: Option<usize>,
640        style: Option<&Style>,
641        new_lines: bool,
642    ) -> Vec<Vec<Segment>> {
643        let height = height.filter(|&height| height > 0).unwrap_or(lines.len());
644        let mut shaped: Vec<Vec<Segment>> = lines
645            .iter()
646            .map(|line| Segment::adjust_line_length(line, width, style.cloned()))
647            .collect();
648        while shaped.len() < height {
649            shaped.push(vec![Segment::blank(width, style, new_lines)]);
650        }
651        shaped
652    }
653
654    /// A blank line of `width` cells in `style`.
655    fn blank(width: usize, style: Option<&Style>, new_lines: bool) -> Segment {
656        let mut text = " ".repeat(width);
657        if new_lines {
658            text.push('\n');
659        }
660        Segment::new(text, style.cloned())
661    }
662
663    /// Lines aligned to the top of `height` rows: blank lines of `style`
664    /// below. Port of `Segment.align_top`.
665    pub fn align_top(
666        lines: &[Vec<Segment>],
667        width: usize,
668        height: usize,
669        style: Option<&Style>,
670        new_lines: bool,
671    ) -> Vec<Vec<Segment>> {
672        Segment::align_vertical(lines, width, height, style, new_lines, |extra| (0, extra))
673    }
674
675    /// Lines aligned to the bottom of `height` rows: blank lines above. Port
676    /// of `Segment.align_bottom`.
677    pub fn align_bottom(
678        lines: &[Vec<Segment>],
679        width: usize,
680        height: usize,
681        style: Option<&Style>,
682        new_lines: bool,
683    ) -> Vec<Vec<Segment>> {
684        Segment::align_vertical(lines, width, height, style, new_lines, |extra| (extra, 0))
685    }
686
687    /// Lines in the middle of `height` rows, the odd blank line below. Port
688    /// of `Segment.align_middle`.
689    pub fn align_middle(
690        lines: &[Vec<Segment>],
691        width: usize,
692        height: usize,
693        style: Option<&Style>,
694        new_lines: bool,
695    ) -> Vec<Vec<Segment>> {
696        Segment::align_vertical(lines, width, height, style, new_lines, |extra| {
697            (extra / 2, extra - extra / 2)
698        })
699    }
700
701    /// The shared body of the `align_*` methods: `split` divides the extra
702    /// rows into (above, below). With exactly `height` rows the lines come
703    /// back unchanged, and with more they are cut to `height`.
704    fn align_vertical(
705        lines: &[Vec<Segment>],
706        width: usize,
707        height: usize,
708        style: Option<&Style>,
709        new_lines: bool,
710        split: impl Fn(usize) -> (usize, usize),
711    ) -> Vec<Vec<Segment>> {
712        if lines.len() >= height {
713            return lines[..height].to_vec();
714        }
715        let (above, below) = split(height - lines.len());
716        let blank = vec![Segment::blank(width, style, new_lines)];
717        let mut aligned = vec![blank.clone(); above];
718        aligned.extend(lines.iter().cloned());
719        aligned.extend(std::iter::repeat_n(blank, below));
720        aligned
721    }
722
723    /// The segments with every hyperlink removed. Port of
724    /// `Segment.strip_links`.
725    pub fn strip_links(segments: &[Segment]) -> Vec<Segment> {
726        segments
727            .iter()
728            .map(|segment| match &segment.style {
729                Some(style) if !segment.control => Segment {
730                    text: segment.text.clone(),
731                    style: (!style.is_null()).then(|| style.update_link(None)),
732                    control: false,
733                },
734                _ => segment.clone(),
735            })
736            .collect()
737    }
738
739    /// The segments with no styles at all. Port of `Segment.strip_styles`.
740    pub fn strip_styles(segments: &[Segment]) -> Vec<Segment> {
741        segments
742            .iter()
743            .map(|segment| Segment {
744                text: segment.text.clone(),
745                style: None,
746                control: segment.control,
747            })
748            .collect()
749    }
750
751    /// Cut the segments at each cell offset in `cuts` (ascending), one list
752    /// per piece; a segment that straddles a cut is split with
753    /// [`split_cells`](Self::split_cells). Port of `Segment.divide`.
754    pub fn divide(segments: &[Segment], cuts: &[usize]) -> Vec<Vec<Segment>> {
755        let mut result = Vec::new();
756        let mut cuts = cuts.iter().copied();
757        let mut cut = loop {
758            match cuts.next() {
759                None => return result,
760                Some(0) => result.push(Vec::new()),
761                Some(cut) => break cut,
762            }
763        };
764        let mut pieces: Vec<Segment> = Vec::new();
765        let mut pos = 0usize;
766        for segment in segments {
767            let mut segment = segment.clone();
768            while !segment.text.is_empty() {
769                let end_pos = if segment.control {
770                    pos
771                } else {
772                    pos + cell_len(&segment.text)
773                };
774                if end_pos < cut {
775                    pieces.push(segment.clone());
776                    pos = end_pos;
777                    break;
778                }
779                if end_pos == cut {
780                    pieces.push(segment.clone());
781                    result.push(std::mem::take(&mut pieces));
782                    pos = end_pos;
783                    match cuts.next() {
784                        Some(next) => cut = next,
785                        None => {
786                            if !pieces.is_empty() {
787                                result.push(pieces);
788                            }
789                            return result;
790                        }
791                    }
792                    break;
793                }
794                let (before, after) = segment.split_cells(cut - pos);
795                segment = after;
796                pieces.push(before);
797                result.push(std::mem::take(&mut pieces));
798                pos = cut;
799                match cuts.next() {
800                    Some(next) => cut = next,
801                    None => {
802                        if !pieces.is_empty() {
803                            result.push(pieces);
804                        }
805                        return result;
806                    }
807                }
808            }
809        }
810        result.push(pieces);
811        result
812    }
813}
814
815/// A list of segments that renders as they are, or one per line with
816/// `new_lines`. Port of `rich.segment.Segments`.
817#[derive(Clone, Debug, Default)]
818pub struct Segments {
819    pub segments: Vec<Segment>,
820    pub new_lines: bool,
821}
822
823impl Segments {
824    pub fn new(segments: Vec<Segment>, new_lines: bool) -> Self {
825        Segments {
826            segments,
827            new_lines,
828        }
829    }
830}
831
832impl crate::protocol::Renderable for Segments {
833    /// Upstream ends each segment with a newline under `new_lines`; this
834    /// port's renderables separate lines, so the last newline is left to
835    /// the caller.
836    fn rich_render(
837        &self,
838        _console: &crate::console::Console,
839        _options: &crate::console::ConsoleOptions,
840    ) -> Vec<Segment> {
841        if !self.new_lines {
842            return self.segments.clone();
843        }
844        let mut out = Vec::with_capacity(self.segments.len() * 2);
845        for (index, segment) in self.segments.iter().enumerate() {
846            if index > 0 {
847                out.push(Segment::line());
848            }
849            out.push(segment.clone());
850        }
851        out
852    }
853}
854
855/// Lines of segments that render one after another, each ending in a
856/// newline with `new_lines`. Port of `rich.segment.SegmentLines`.
857#[derive(Clone, Debug, Default)]
858pub struct SegmentLines {
859    pub lines: Vec<Vec<Segment>>,
860    pub new_lines: bool,
861}
862
863impl SegmentLines {
864    pub fn new(lines: Vec<Vec<Segment>>, new_lines: bool) -> Self {
865        SegmentLines { lines, new_lines }
866    }
867}
868
869impl crate::protocol::Renderable for SegmentLines {
870    /// As [`Segments`], the last line's newline is left to the caller.
871    fn rich_render(
872        &self,
873        _console: &crate::console::Console,
874        _options: &crate::console::ConsoleOptions,
875    ) -> Vec<Segment> {
876        let mut out = Vec::new();
877        for (index, line) in self.lines.iter().enumerate() {
878            if index > 0 && self.new_lines {
879                out.push(Segment::line());
880            }
881            out.extend(line.iter().cloned());
882        }
883        out
884    }
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    /// Moving the segments splits them as cloning them does, on every
892    /// sequence of up to five segments drawn from the kinds that matter.
893    #[test]
894    fn split_lines_owned_matches_split_lines() {
895        let bold = Style::parse("bold").unwrap();
896        let kinds = [
897            Segment::new("ab", None),
898            Segment::new("", None),
899            Segment::new("\n", Some(bold.clone())),
900            Segment::new("x\ny\n", Some(bold)),
901            Segment::new("\n\nz", None),
902            Segment::line(),
903            Segment::control("\x1b[2J"),
904        ];
905        let mut sequences: Vec<Vec<Segment>> = vec![Vec::new()];
906        for _ in 0..5 {
907            sequences = sequences
908                .iter()
909                .flat_map(|sequence| {
910                    kinds.iter().map(move |kind| {
911                        let mut next = sequence.clone();
912                        next.push(kind.clone());
913                        next
914                    })
915                })
916                .collect();
917            for sequence in &sequences {
918                assert_eq!(
919                    Segment::split_lines_owned(sequence.clone()),
920                    Segment::split_lines(sequence),
921                    "{sequence:?}"
922                );
923            }
924        }
925    }
926
927    /// Cropping is per line, leaves short lines alone, and keeps zero-width
928    /// control segments so cursor moves survive.
929    #[test]
930    fn crop_lines_cuts_each_line_independently() {
931        let segments = vec![
932            Segment::new("hello world", None),
933            Segment::line(),
934            Segment::new("hi", None),
935            Segment::line(),
936            Segment::control("\x1b[2A"),
937            Segment::new("abcdefgh", None),
938        ];
939        let cropped = Segment::crop_lines(&segments, 5);
940        let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
941        assert_eq!(texts, vec!["hello", "\n", "hi", "\n", "\x1b[2A", "abcde"]);
942    }
943
944    /// A print `end` such as `"!!\n"` arrives as one segment; it is split
945    /// at the newline before cropping, so the newline survives
946    /// (`print("xy", end="!!\n")` at width 3 is `"xy!\n"`).
947    #[test]
948    fn crop_lines_keeps_a_newline_inside_a_segment() {
949        let segments = vec![Segment::new("xy", None), Segment::new("!!\n", None)];
950        let cropped = Segment::crop_lines(&segments, 3);
951        let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
952        assert_eq!(texts, vec!["xy", "!", "\n"]);
953    }
954
955    /// Past the crop edge everything goes, zero-width characters included,
956    /// as upstream's `adjust_line_length` breaks at the edge.
957    #[test]
958    fn crop_lines_drops_zero_width_characters_past_the_edge() {
959        let segments = vec![
960            Segment::new("abc", None),
961            Segment::new("\u{200b}", None),
962            Segment::new("d", None),
963        ];
964        let cropped = Segment::crop_lines(&segments, 3);
965        let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
966        assert_eq!(texts, vec!["abc"]);
967        // A line that fits is left whole, trailing zero-width included.
968        let fits = Segment::crop_lines(&segments[..2], 3);
969        assert_eq!(fits.len(), 2);
970    }
971
972    /// A wide character straddling the crop is dropped whole and its cell padded,
973    /// so the line still occupies exactly the requested width.
974    #[test]
975    fn crop_lines_pads_a_split_wide_character() {
976        let segments = vec![Segment::new("aa你好", None)];
977        let cropped = Segment::crop_lines(&segments, 5);
978        assert_eq!(cropped[0].text, "aa你 ");
979    }
980
981    /// A crop boundary falling between segments keeps the styles of the ones it
982    /// kept and drops the rest entirely.
983    #[test]
984    fn crop_lines_preserves_styles_and_drops_the_tail() {
985        let bold = Style::parse("bold").unwrap();
986        let segments = vec![
987            Segment::new("abc", Some(bold.clone())),
988            Segment::new("defgh", None),
989        ];
990        let cropped = Segment::crop_lines(&segments, 3);
991        assert_eq!(cropped.len(), 1);
992        assert_eq!(cropped[0].text, "abc");
993        assert_eq!(cropped[0].style, Some(bold));
994    }
995
996    #[test]
997    fn cell_length_ignores_control() {
998        assert_eq!(Segment::new("abc", None).cell_length(), 3);
999        assert_eq!(Segment::control("\x1b[2J").cell_length(), 0);
1000    }
1001
1002    #[test]
1003    fn fold_lines_carries_the_overflow_instead_of_dropping_it() {
1004        let segments = vec![Segment::new("abcdefghij", None)];
1005        let folded = Segment::fold_lines(&segments, 4);
1006        let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1007        // Every character survives; only line breaks are added.
1008        assert_eq!(text.replace('\n', ""), "abcdefghij");
1009        assert_eq!(Segment::split_lines(&folded).len(), 3);
1010    }
1011
1012    #[test]
1013    fn fold_lines_preserves_styles_across_a_break() {
1014        let style = Style::parse("bold").expect("valid style");
1015        let segments = vec![Segment::new("abcdef", Some(style.clone()))];
1016        let folded = Segment::fold_lines(&segments, 3);
1017        for segment in folded.iter().filter(|s| !s.text.contains('\n')) {
1018            assert_eq!(segment.style.as_ref(), Some(&style), "style lost on fold");
1019        }
1020    }
1021
1022    /// A glyph wider than the whole row is emitted anyway, overflowing — but as
1023    /// a whole grapheme. Taking a single code point off `"❤️"` put the bare `❤`
1024    /// on one row and stranded the variation selector at the start of the next,
1025    /// where it silently re-widens whatever character follows it.
1026    #[test]
1027    fn fold_lines_never_splits_a_grapheme() {
1028        let heart = "\u{2764}\u{fe0f}";
1029        let segments = vec![Segment::new(heart.repeat(3), None)];
1030        let folded = Segment::fold_lines(&segments, 1);
1031        let rows: Vec<String> = Segment::split_lines(&folded)
1032            .iter()
1033            .map(|line| line.iter().map(|s| s.text.as_str()).collect())
1034            .collect();
1035        assert_eq!(rows, vec![heart, heart, heart]);
1036    }
1037
1038    #[test]
1039    fn crop_lines_still_drops_the_overflow() {
1040        // fold_lines is the alternative, not a replacement: crop stays the
1041        // display backstop for renderables that already wrapped themselves.
1042        let segments = vec![Segment::new("abcdefghij", None)];
1043        let cropped = Segment::crop_lines(&segments, 4);
1044        let text: String = cropped.iter().map(|s| s.text.as_str()).collect();
1045        assert_eq!(text, "abcd");
1046    }
1047
1048    #[test]
1049    fn fold_lines_terminates_when_a_glyph_is_wider_than_the_width() {
1050        // A 2-cell character with 1 column available used to loop forever,
1051        // pushing a line break per iteration (~400 MB/s until killed). Every
1052        // branch of the fold loop must consume input.
1053        let segments = vec![Segment::new("\u{4f60}\u{4f60}", None)];
1054        let folded = Segment::fold_lines(&segments, 1);
1055        let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1056        assert_eq!(
1057            text.matches('\u{4f60}').count(),
1058            2,
1059            "both characters should survive, overflowing rather than looping"
1060        );
1061    }
1062
1063    /// The word-wrapping fold breaks *between* words, leaving the space that
1064    /// separated them at the end of the finished row — exactly where
1065    /// `_wrap.divide_line` puts the offset.
1066    #[test]
1067    fn fold_lines_words_breaks_between_words() {
1068        let segments = vec![Segment::new("the quick brown fox", None)];
1069        // 12, not 10: at 10 a character fold would land on the same boundary by
1070        // luck and the test would pass either way.
1071        let folded = Segment::fold_lines_words(&segments, 12);
1072        let lines: Vec<String> = Segment::split_lines(&folded)
1073            .iter()
1074            .map(|line| line.iter().map(|s| s.text.as_str()).collect())
1075            .collect();
1076        assert_eq!(lines, vec!["the quick ", "brown fox"]);
1077    }
1078
1079    /// A break landing inside a styled run must not drop the style, or a wrapped
1080    /// JSON string would lose its colour halfway down.
1081    #[test]
1082    fn fold_lines_words_preserves_styles_across_a_break() {
1083        let green = Style::parse("green").expect("valid style");
1084        let segments = vec![
1085            Segment::new("key: ", None),
1086            Segment::new("alpha beta gamma", Some(green.clone())),
1087        ];
1088        let folded = Segment::fold_lines_words(&segments, 12);
1089        let styled: String = folded
1090            .iter()
1091            .filter(|s| s.style.as_ref() == Some(&green))
1092            .map(|s| s.text.as_str())
1093            .collect();
1094        assert_eq!(styled, "alpha beta gamma", "style lost across the break");
1095    }
1096
1097    /// Nothing may be dropped: a word wider than the row still has to fold, and
1098    /// the offsets have to line up with the segments they cut.
1099    #[test]
1100    fn fold_lines_words_keeps_every_character() {
1101        let segments = vec![
1102            Segment::new("short ", None),
1103            Segment::new("z".repeat(25), None),
1104            Segment::new(" tail", None),
1105        ];
1106        for width in 1..=30 {
1107            let folded = Segment::fold_lines_words(&segments, width);
1108            let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1109            assert_eq!(
1110                text.replace('\n', ""),
1111                format!("short {} tail", "z".repeat(25)),
1112                "width {width} lost or reordered characters"
1113            );
1114        }
1115    }
1116
1117    /// Control segments carry no cells, so they must ride through untouched
1118    /// rather than count against the width or vanish.
1119    #[test]
1120    fn fold_lines_words_keeps_control_segments() {
1121        let segments = vec![
1122            Segment::control("\x1b]8;;http://x\x1b\\"),
1123            Segment::new("alpha beta", None),
1124        ];
1125        let folded = Segment::fold_lines_words(&segments, 8);
1126        assert_eq!(folded.iter().filter(|s| s.control).count(), 1);
1127        let text: String = folded
1128            .iter()
1129            .filter(|s| !s.control)
1130            .map(|s| s.text.as_str())
1131            .collect();
1132        assert_eq!(text, "alpha \nbeta");
1133    }
1134
1135    #[test]
1136    fn fold_lines_terminates_at_every_narrow_width() {
1137        // Mixed widths: ASCII, CJK, and an emoji, folded at each width from 1.
1138        let sample = "a\u{4f60}b\u{1f600}c";
1139        for width in 1..=6 {
1140            let folded = Segment::fold_lines(&[Segment::new(sample, None)], width);
1141            let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1142            assert!(
1143                text.contains('c'),
1144                "width {width} lost the tail, or did not terminate"
1145            );
1146        }
1147    }
1148}