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    /// Crop every line in a segment stream to at most `width` cells, discarding
150    /// the excess and leaving short lines alone.
151    ///
152    /// Port of `Segment.split_and_crop_lines` with `pad=False`, which is what
153    /// `Console.print(crop=True)` applies to the finished stream. It is the only
154    /// thing standing between an [`Overflow::Ignore`](crate::console::Overflow)
155    /// text and a line that runs off the side of the terminal.
156    ///
157    /// Control segments occupy no cells and are always kept, so cursor moves and
158    /// hyperlink codes survive a crop.
159    pub fn crop_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
160        let mut result: Vec<Segment> = Vec::with_capacity(segments.len());
161        let mut used = 0usize;
162        for segment in segments {
163            if segment.control {
164                result.push(segment.clone());
165                continue;
166            }
167            if segment.text == "\n" {
168                used = 0;
169                result.push(segment.clone());
170                continue;
171            }
172            let length = segment.cell_length();
173            if used + length <= width {
174                used += length;
175                result.push(segment.clone());
176            } else if used < width {
177                // Straddles the crop: keep the part that fits. A wide character
178                // across the boundary is dropped and the gap padded, as
179                // `set_cell_size` does everywhere else.
180                result.push(Segment::new(
181                    crate::cells::set_cell_size(&segment.text, width - used),
182                    segment.style.clone(),
183                ));
184                used = width;
185            }
186            // Anything else is wholly past the crop, so it is dropped.
187        }
188        result
189    }
190
191    /// Pad (with a styled space run) or crop a single line to exactly `length`
192    /// cells. Port of `Segment.adjust_line_length`.
193    pub fn adjust_line_length(
194        line: &[Segment],
195        length: usize,
196        style: Option<Style>,
197    ) -> Vec<Segment> {
198        let line_length: usize = line.iter().map(Segment::cell_length).sum();
199        if line_length == length {
200            line.to_vec()
201        } else if line_length < length {
202            let mut new_line = line.to_vec();
203            new_line.push(Segment::new(" ".repeat(length - line_length), style));
204            new_line
205        } else {
206            // Crop from the left, honoring cell widths.
207            let mut new_line: Vec<Segment> = Vec::new();
208            let mut remaining = length;
209            for segment in line {
210                let seg_len = segment.cell_length();
211                if seg_len <= remaining {
212                    new_line.push(segment.clone());
213                    remaining -= seg_len;
214                } else {
215                    let cropped = crate::cells::set_cell_size(&segment.text, remaining);
216                    new_line.push(Segment::new(cropped, segment.style.clone()));
217                    break;
218                }
219            }
220            new_line
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    /// Cropping is per line, leaves short lines alone, and keeps zero-width
230    /// control segments so cursor moves survive.
231    #[test]
232    fn crop_lines_cuts_each_line_independently() {
233        let segments = vec![
234            Segment::new("hello world", None),
235            Segment::line(),
236            Segment::new("hi", None),
237            Segment::line(),
238            Segment::control("\x1b[2A"),
239            Segment::new("abcdefgh", None),
240        ];
241        let cropped = Segment::crop_lines(&segments, 5);
242        let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
243        assert_eq!(texts, vec!["hello", "\n", "hi", "\n", "\x1b[2A", "abcde"]);
244    }
245
246    /// A wide character straddling the crop is dropped whole and its cell padded,
247    /// so the line still occupies exactly the requested width.
248    #[test]
249    fn crop_lines_pads_a_split_wide_character() {
250        let segments = vec![Segment::new("aa你好", None)];
251        let cropped = Segment::crop_lines(&segments, 5);
252        assert_eq!(cropped[0].text, "aa你 ");
253    }
254
255    /// A crop boundary falling between segments keeps the styles of the ones it
256    /// kept and drops the rest entirely.
257    #[test]
258    fn crop_lines_preserves_styles_and_drops_the_tail() {
259        let bold = Style::parse("bold").unwrap();
260        let segments = vec![
261            Segment::new("abc", Some(bold.clone())),
262            Segment::new("defgh", None),
263        ];
264        let cropped = Segment::crop_lines(&segments, 3);
265        assert_eq!(cropped.len(), 1);
266        assert_eq!(cropped[0].text, "abc");
267        assert_eq!(cropped[0].style, Some(bold));
268    }
269
270    #[test]
271    fn cell_length_ignores_control() {
272        assert_eq!(Segment::new("abc", None).cell_length(), 3);
273        assert_eq!(Segment::control("\x1b[2J").cell_length(), 0);
274    }
275}