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    pub text: String,
17    pub style: Option<Style>,
18    /// Whether this segment carries terminal control codes rather than content.
19    pub control: bool,
20}
21
22impl Segment {
23    /// A plain content segment.
24    pub fn new(text: impl Into<String>, style: Option<Style>) -> Self {
25        Segment {
26            text: text.into(),
27            style,
28            control: false,
29        }
30    }
31
32    /// A newline segment (`Segment.line()` upstream).
33    pub fn line() -> Self {
34        Segment {
35            text: "\n".to_string(),
36            style: None,
37            control: false,
38        }
39    }
40
41    /// A control segment (carries no visible width).
42    pub fn control(text: impl Into<String>) -> Self {
43        Segment {
44            text: text.into(),
45            style: None,
46            control: true,
47        }
48    }
49
50    /// The number of terminal cells this segment occupies (0 for control).
51    pub fn cell_length(&self) -> usize {
52        if self.control {
53            0
54        } else {
55            cell_len(&self.text)
56        }
57    }
58
59    /// Merge adjacent segments that share the same style and control flag.
60    /// Port of `Segment.simplify`.
61    pub fn simplify(segments: &[Segment]) -> Vec<Segment> {
62        let mut out: Vec<Segment> = Vec::with_capacity(segments.len());
63        for segment in segments {
64            match out.last_mut() {
65                Some(last) if last.style == segment.style && last.control == segment.control => {
66                    last.text.push_str(&segment.text);
67                }
68                _ => out.push(segment.clone()),
69            }
70        }
71        out
72    }
73
74    /// Apply `style` as a base *under* each segment's own style (that segment's
75    /// style wins on top). Control segments are left untouched. Port of
76    /// `Segment.apply_style` (the `style`-only path).
77    ///
78    /// Line-break segments (`"\n"`) are also left unstyled: upstream's
79    /// line-oriented print pipeline re-emits row separators plain, so styling
80    /// them would add stray SGR runs around every newline.
81    pub fn apply_style(segments: &[Segment], style: &Style) -> Vec<Segment> {
82        segments
83            .iter()
84            .map(|segment| {
85                if segment.control || segment.text == "\n" {
86                    segment.clone()
87                } else {
88                    let combined = match &segment.style {
89                        Some(own) => style.combine(own),
90                        None => style.clone(),
91                    };
92                    Segment {
93                        text: segment.text.clone(),
94                        style: Some(combined),
95                        control: false,
96                    }
97                }
98            })
99            .collect()
100    }
101
102    /// Split a flat segment stream into lines, breaking on `\n`.
103    ///
104    /// Port of `Segment.split_lines`. Newline characters are consumed (not kept
105    /// in the output); a trailing newline yields a final empty line only if
106    /// there was content after the last break.
107    pub fn split_lines(segments: &[Segment]) -> Vec<Vec<Segment>> {
108        let mut lines: Vec<Vec<Segment>> = Vec::new();
109        let mut current: Vec<Segment> = Vec::new();
110        for segment in segments {
111            if segment.control || !segment.text.contains('\n') {
112                if !segment.text.is_empty() {
113                    current.push(segment.clone());
114                }
115                continue;
116            }
117            let mut parts = segment.text.split('\n').peekable();
118            while let Some(part) = parts.next() {
119                if !part.is_empty() {
120                    current.push(Segment::new(part, segment.style.clone()));
121                }
122                if parts.peek().is_some() {
123                    // The break between parts closes the current line.
124                    lines.push(std::mem::take(&mut current));
125                }
126            }
127        }
128        if !current.is_empty() {
129            lines.push(current);
130        }
131        lines
132    }
133
134    /// Shape a set of lines into exactly `height` rows of `width` cells: crop
135    /// extra rows, pad each row to `width`, and append blank rows to reach
136    /// `height`. Port of `Segment.set_shape` (`style=None`, `new_lines=False`).
137    pub fn set_shape(lines: Vec<Vec<Segment>>, width: usize, height: usize) -> Vec<Vec<Segment>> {
138        let mut shaped: Vec<Vec<Segment>> = lines
139            .into_iter()
140            .take(height)
141            .map(|line| Segment::adjust_line_length(&line, width, None))
142            .collect();
143        while shaped.len() < height {
144            shaped.push(vec![Segment::new(" ".repeat(width), None)]);
145        }
146        shaped
147    }
148
149    /// Fold every line to at most `width` cells, breaking at **word boundaries**
150    /// the way upstream's word wrapping does.
151    ///
152    /// [`fold_lines`](Self::fold_lines) breaks wherever the row happens to fill
153    /// up, which splits identifiers and words mid-character-run
154    /// (`epsilon, z` / `eta, eta, theta)`). Upstream's `word_wrap=True` routes
155    /// through `_wrap.divide_line`, which we already port for `Text` — this
156    /// applies the same break offsets to a styled segment run, so styles survive
157    /// the split.
158    ///
159    /// A word longer than `width` is still folded mid-word; there is nowhere
160    /// else to break it.
161    pub fn fold_lines_words(segments: &[Segment], width: usize) -> Vec<Segment> {
162        if width == 0 {
163            return segments.to_vec();
164        }
165        let mut out = Vec::new();
166        let lines = Self::split_lines(segments);
167        let last = lines.len().saturating_sub(1);
168        for (index, line) in lines.into_iter().enumerate() {
169            let plain: String = line
170                .iter()
171                .filter(|segment| !segment.control)
172                .map(|segment| segment.text.as_str())
173                .collect();
174            let breaks = crate::wrap::divide_line(&plain, width, true);
175
176            let mut char_pos = 0usize;
177            let mut next_break = 0usize;
178            for segment in line {
179                if segment.control {
180                    out.push(segment);
181                    continue;
182                }
183                let mut buf = String::new();
184                for ch in segment.text.chars() {
185                    while next_break < breaks.len() && char_pos == breaks[next_break] {
186                        if !buf.is_empty() {
187                            out.push(Segment::new(buf.clone(), segment.style.clone()));
188                            buf.clear();
189                        }
190                        out.push(Segment::line());
191                        next_break += 1;
192                    }
193                    buf.push(ch);
194                    char_pos += 1;
195                }
196                if !buf.is_empty() {
197                    out.push(Segment::new(buf, segment.style.clone()));
198                }
199            }
200            if index != last {
201                out.push(Segment::line());
202            }
203        }
204        out
205    }
206
207    /// Fold every line to at most `width` cells, carrying the overflow onto
208    /// continuation lines instead of discarding it.
209    ///
210    /// [`crop_lines`](Self::crop_lines) is the display backstop and **throws the
211    /// remainder away** — correct for a renderable that has already wrapped
212    /// itself, and data loss for one that emits a long line verbatim. Styles are
213    /// preserved across the split.
214    ///
215    /// This breaks wherever the row happens to fill up, so it splits words. That
216    /// is right only for content upstream folds *without* word wrapping. A
217    /// renderable whose upstream counterpart goes through `Text.wrap` wants
218    /// [`fold_lines_words`](Self::fold_lines_words) instead: reaching for this
219    /// one is what made `--json` print `over t` / `he lazy` where rich prints
220    /// `over ` / `the lazy`.
221    pub fn fold_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
222        if width == 0 {
223            return segments.to_vec();
224        }
225        let mut out = Vec::new();
226        let lines = Self::split_lines(segments);
227        let last = lines.len().saturating_sub(1);
228        for (index, line) in lines.into_iter().enumerate() {
229            let mut used = 0usize;
230            for segment in line {
231                if segment.control {
232                    out.push(segment);
233                    continue;
234                }
235                // Walk the segment in cell-sized pieces, breaking whenever the
236                // current row is full.
237                let mut remaining = segment.text.as_str();
238                while !remaining.is_empty() {
239                    let room = width.saturating_sub(used);
240                    if room == 0 {
241                        out.push(Segment::line());
242                        used = 0;
243                        continue;
244                    }
245                    let chunks = crate::cells::chop_cells(remaining, room);
246                    let mut head = chunks.first().cloned().unwrap_or_default();
247                    if head.is_empty() {
248                        if used > 0 {
249                            // The row has content but no space for this
250                            // character; start a fresh one and try again.
251                            out.push(Segment::line());
252                            used = 0;
253                            continue;
254                        }
255                        // Already at the start of a row and the glyph STILL does
256                        // not fit — a 2-cell glyph at width 1. Emit it anyway,
257                        // overflowing by a cell.
258                        //
259                        // A whole *grapheme*, not a single code point: taking one
260                        // code point off `"❤️"` emits a bare `❤` and leaves a
261                        // stranded variation selector to be emitted on the next
262                        // row, where it silently re-widens whatever character
263                        // precedes it.
264                        //
265                        // Retrying here instead was an infinite loop that
266                        // allocated a line break per iteration: ~400 MB/s until
267                        // the process was killed. Every branch of this loop must
268                        // consume input.
269                        let (spans, _) = crate::cells::split_graphemes(remaining);
270                        let take = spans.first().map_or(remaining.len(), |span| span.1);
271                        head = remaining[..take].to_string();
272                    }
273                    used += crate::cells::cell_len(&head);
274                    remaining = &remaining[head.len()..];
275                    out.push(Segment::new(head, segment.style.clone()));
276                    if !remaining.is_empty() {
277                        out.push(Segment::line());
278                        used = 0;
279                    }
280                }
281            }
282            if index != last {
283                out.push(Segment::line());
284            }
285        }
286        out
287    }
288
289    /// Crop every line in a segment stream to at most `width` cells, discarding
290    /// the excess and leaving short lines alone.
291    ///
292    /// Port of `Segment.split_and_crop_lines` with `pad=False`, which is what
293    /// `Console.print(crop=True)` applies to the finished stream. It is the only
294    /// thing standing between an [`Overflow::Ignore`](crate::console::Overflow)
295    /// text and a line that runs off the side of the terminal.
296    ///
297    /// Control segments occupy no cells and are always kept, so cursor moves and
298    /// hyperlink codes survive a crop.
299    pub fn crop_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
300        let mut result: Vec<Segment> = Vec::with_capacity(segments.len());
301        let mut used = 0usize;
302        for segment in segments {
303            if segment.control {
304                result.push(segment.clone());
305                continue;
306            }
307            if segment.text == "\n" {
308                used = 0;
309                result.push(segment.clone());
310                continue;
311            }
312            let length = segment.cell_length();
313            if used + length <= width {
314                used += length;
315                result.push(segment.clone());
316            } else if used < width {
317                // Straddles the crop: keep the part that fits. A wide character
318                // across the boundary is dropped and the gap padded, as
319                // `set_cell_size` does everywhere else.
320                result.push(Segment::new(
321                    crate::cells::set_cell_size(&segment.text, width - used),
322                    segment.style.clone(),
323                ));
324                used = width;
325            }
326            // Anything else is wholly past the crop, so it is dropped.
327        }
328        result
329    }
330
331    /// Pad (with a styled space run) or crop a single line to exactly `length`
332    /// cells. Port of `Segment.adjust_line_length`.
333    pub fn adjust_line_length(
334        line: &[Segment],
335        length: usize,
336        style: Option<Style>,
337    ) -> Vec<Segment> {
338        let line_length: usize = line.iter().map(Segment::cell_length).sum();
339        if line_length == length {
340            line.to_vec()
341        } else if line_length < length {
342            let mut new_line = line.to_vec();
343            new_line.push(Segment::new(" ".repeat(length - line_length), style));
344            new_line
345        } else {
346            // Crop from the left, honoring cell widths.
347            let mut new_line: Vec<Segment> = Vec::new();
348            let mut remaining = length;
349            for segment in line {
350                let seg_len = segment.cell_length();
351                if seg_len <= remaining {
352                    new_line.push(segment.clone());
353                    remaining -= seg_len;
354                } else {
355                    let cropped = crate::cells::set_cell_size(&segment.text, remaining);
356                    new_line.push(Segment::new(cropped, segment.style.clone()));
357                    break;
358                }
359            }
360            new_line
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    /// Cropping is per line, leaves short lines alone, and keeps zero-width
370    /// control segments so cursor moves survive.
371    #[test]
372    fn crop_lines_cuts_each_line_independently() {
373        let segments = vec![
374            Segment::new("hello world", None),
375            Segment::line(),
376            Segment::new("hi", None),
377            Segment::line(),
378            Segment::control("\x1b[2A"),
379            Segment::new("abcdefgh", None),
380        ];
381        let cropped = Segment::crop_lines(&segments, 5);
382        let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
383        assert_eq!(texts, vec!["hello", "\n", "hi", "\n", "\x1b[2A", "abcde"]);
384    }
385
386    /// A wide character straddling the crop is dropped whole and its cell padded,
387    /// so the line still occupies exactly the requested width.
388    #[test]
389    fn crop_lines_pads_a_split_wide_character() {
390        let segments = vec![Segment::new("aa你好", None)];
391        let cropped = Segment::crop_lines(&segments, 5);
392        assert_eq!(cropped[0].text, "aa你 ");
393    }
394
395    /// A crop boundary falling between segments keeps the styles of the ones it
396    /// kept and drops the rest entirely.
397    #[test]
398    fn crop_lines_preserves_styles_and_drops_the_tail() {
399        let bold = Style::parse("bold").unwrap();
400        let segments = vec![
401            Segment::new("abc", Some(bold.clone())),
402            Segment::new("defgh", None),
403        ];
404        let cropped = Segment::crop_lines(&segments, 3);
405        assert_eq!(cropped.len(), 1);
406        assert_eq!(cropped[0].text, "abc");
407        assert_eq!(cropped[0].style, Some(bold));
408    }
409
410    #[test]
411    fn cell_length_ignores_control() {
412        assert_eq!(Segment::new("abc", None).cell_length(), 3);
413        assert_eq!(Segment::control("\x1b[2J").cell_length(), 0);
414    }
415
416    #[test]
417    fn fold_lines_carries_the_overflow_instead_of_dropping_it() {
418        let segments = vec![Segment::new("abcdefghij", None)];
419        let folded = Segment::fold_lines(&segments, 4);
420        let text: String = folded.iter().map(|s| s.text.as_str()).collect();
421        // Every character survives; only line breaks are added.
422        assert_eq!(text.replace('\n', ""), "abcdefghij");
423        assert_eq!(Segment::split_lines(&folded).len(), 3);
424    }
425
426    #[test]
427    fn fold_lines_preserves_styles_across_a_break() {
428        let style = Style::parse("bold").expect("valid style");
429        let segments = vec![Segment::new("abcdef", Some(style.clone()))];
430        let folded = Segment::fold_lines(&segments, 3);
431        for segment in folded.iter().filter(|s| !s.text.contains('\n')) {
432            assert_eq!(segment.style.as_ref(), Some(&style), "style lost on fold");
433        }
434    }
435
436    /// A glyph wider than the whole row is emitted anyway, overflowing — but as
437    /// a whole grapheme. Taking a single code point off `"❤️"` put the bare `❤`
438    /// on one row and stranded the variation selector at the start of the next,
439    /// where it silently re-widens whatever character follows it.
440    #[test]
441    fn fold_lines_never_splits_a_grapheme() {
442        let heart = "\u{2764}\u{fe0f}";
443        let segments = vec![Segment::new(heart.repeat(3), None)];
444        let folded = Segment::fold_lines(&segments, 1);
445        let rows: Vec<String> = Segment::split_lines(&folded)
446            .iter()
447            .map(|line| line.iter().map(|s| s.text.as_str()).collect())
448            .collect();
449        assert_eq!(rows, vec![heart, heart, heart]);
450    }
451
452    #[test]
453    fn crop_lines_still_drops_the_overflow() {
454        // fold_lines is the alternative, not a replacement: crop stays the
455        // display backstop for renderables that already wrapped themselves.
456        let segments = vec![Segment::new("abcdefghij", None)];
457        let cropped = Segment::crop_lines(&segments, 4);
458        let text: String = cropped.iter().map(|s| s.text.as_str()).collect();
459        assert_eq!(text, "abcd");
460    }
461
462    #[test]
463    fn fold_lines_terminates_when_a_glyph_is_wider_than_the_width() {
464        // A 2-cell character with 1 column available used to loop forever,
465        // pushing a line break per iteration (~400 MB/s until killed). Every
466        // branch of the fold loop must consume input.
467        let segments = vec![Segment::new("\u{4f60}\u{4f60}", None)];
468        let folded = Segment::fold_lines(&segments, 1);
469        let text: String = folded.iter().map(|s| s.text.as_str()).collect();
470        assert_eq!(
471            text.matches('\u{4f60}').count(),
472            2,
473            "both characters should survive, overflowing rather than looping"
474        );
475    }
476
477    /// The word-wrapping fold breaks *between* words, leaving the space that
478    /// separated them at the end of the finished row — exactly where
479    /// `_wrap.divide_line` puts the offset.
480    #[test]
481    fn fold_lines_words_breaks_between_words() {
482        let segments = vec![Segment::new("the quick brown fox", None)];
483        // 12, not 10: at 10 a character fold would land on the same boundary by
484        // luck and the test would pass either way.
485        let folded = Segment::fold_lines_words(&segments, 12);
486        let lines: Vec<String> = Segment::split_lines(&folded)
487            .iter()
488            .map(|line| line.iter().map(|s| s.text.as_str()).collect())
489            .collect();
490        assert_eq!(lines, vec!["the quick ", "brown fox"]);
491    }
492
493    /// A break landing inside a styled run must not drop the style, or a wrapped
494    /// JSON string would lose its colour halfway down.
495    #[test]
496    fn fold_lines_words_preserves_styles_across_a_break() {
497        let green = Style::parse("green").expect("valid style");
498        let segments = vec![
499            Segment::new("key: ", None),
500            Segment::new("alpha beta gamma", Some(green.clone())),
501        ];
502        let folded = Segment::fold_lines_words(&segments, 12);
503        let styled: String = folded
504            .iter()
505            .filter(|s| s.style.as_ref() == Some(&green))
506            .map(|s| s.text.as_str())
507            .collect();
508        assert_eq!(styled, "alpha beta gamma", "style lost across the break");
509    }
510
511    /// Nothing may be dropped: a word wider than the row still has to fold, and
512    /// the offsets have to line up with the segments they cut.
513    #[test]
514    fn fold_lines_words_keeps_every_character() {
515        let segments = vec![
516            Segment::new("short ", None),
517            Segment::new("z".repeat(25), None),
518            Segment::new(" tail", None),
519        ];
520        for width in 1..=30 {
521            let folded = Segment::fold_lines_words(&segments, width);
522            let text: String = folded.iter().map(|s| s.text.as_str()).collect();
523            assert_eq!(
524                text.replace('\n', ""),
525                format!("short {} tail", "z".repeat(25)),
526                "width {width} lost or reordered characters"
527            );
528        }
529    }
530
531    /// Control segments carry no cells, so they must ride through untouched
532    /// rather than count against the width or vanish.
533    #[test]
534    fn fold_lines_words_keeps_control_segments() {
535        let segments = vec![
536            Segment::control("\x1b]8;;http://x\x1b\\"),
537            Segment::new("alpha beta", None),
538        ];
539        let folded = Segment::fold_lines_words(&segments, 8);
540        assert_eq!(folded.iter().filter(|s| s.control).count(), 1);
541        let text: String = folded
542            .iter()
543            .filter(|s| !s.control)
544            .map(|s| s.text.as_str())
545            .collect();
546        assert_eq!(text, "alpha \nbeta");
547    }
548
549    #[test]
550    fn fold_lines_terminates_at_every_narrow_width() {
551        // Mixed widths: ASCII, CJK, and an emoji, folded at each width from 1.
552        let sample = "a\u{4f60}b\u{1f600}c";
553        for width in 1..=6 {
554            let folded = Segment::fold_lines(&[Segment::new(sample, None)], width);
555            let text: String = folded.iter().map(|s| s.text.as_str()).collect();
556            assert!(
557                text.contains('c'),
558                "width {width} lost the tail, or did not terminate"
559            );
560        }
561    }
562}