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