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