1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! Segments — the atoms of rendering.
//!
//! Port of upstream `rich/segment.py`. A [`Segment`] is a piece of text with an
//! optional [`Style`]. Everything renderable ultimately becomes a stream of
//! segments, which the [`Console`](crate::console::Console) turns into bytes.
//!
//! Control-code segments carry a `control` flag; the typed control sequences
//! that populate them live in [`control`](crate::control).
use crate::cells::cell_len;
use crate::style::Style;
/// A span of text with an optional style. Mirrors `rich.segment.Segment`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment {
pub text: String,
pub style: Option<Style>,
/// Whether this segment carries terminal control codes rather than content.
pub control: bool,
}
impl Segment {
/// A plain content segment.
pub fn new(text: impl Into<String>, style: Option<Style>) -> Self {
Segment {
text: text.into(),
style,
control: false,
}
}
/// A newline segment (`Segment.line()` upstream).
pub fn line() -> Self {
Segment {
text: "\n".to_string(),
style: None,
control: false,
}
}
/// A control segment (carries no visible width).
pub fn control(text: impl Into<String>) -> Self {
Segment {
text: text.into(),
style: None,
control: true,
}
}
/// The number of terminal cells this segment occupies (0 for control).
pub fn cell_length(&self) -> usize {
if self.control {
0
} else {
cell_len(&self.text)
}
}
/// Merge adjacent segments that share the same style and control flag.
/// Port of `Segment.simplify`.
pub fn simplify(segments: &[Segment]) -> Vec<Segment> {
let mut out: Vec<Segment> = Vec::with_capacity(segments.len());
for segment in segments {
match out.last_mut() {
Some(last) if last.style == segment.style && last.control == segment.control => {
last.text.push_str(&segment.text);
}
_ => out.push(segment.clone()),
}
}
out
}
/// Apply `style` as a base *under* each segment's own style (that segment's
/// style wins on top). Control segments are left untouched. Port of
/// `Segment.apply_style` (the `style`-only path).
///
/// Line-break segments (`"\n"`) are also left unstyled: upstream's
/// line-oriented print pipeline re-emits row separators plain, so styling
/// them would add stray SGR runs around every newline.
pub fn apply_style(segments: &[Segment], style: &Style) -> Vec<Segment> {
segments
.iter()
.map(|segment| {
if segment.control || segment.text == "\n" {
segment.clone()
} else {
let combined = match &segment.style {
Some(own) => style.combine(own),
None => style.clone(),
};
Segment {
text: segment.text.clone(),
style: Some(combined),
control: false,
}
}
})
.collect()
}
/// Split a flat segment stream into lines, breaking on `\n`.
///
/// Port of `Segment.split_lines`. Newline characters are consumed (not kept
/// in the output); a trailing newline yields a final empty line only if
/// there was content after the last break.
pub fn split_lines(segments: &[Segment]) -> Vec<Vec<Segment>> {
let mut lines: Vec<Vec<Segment>> = Vec::new();
let mut current: Vec<Segment> = Vec::new();
for segment in segments {
if segment.control || !segment.text.contains('\n') {
if !segment.text.is_empty() {
current.push(segment.clone());
}
continue;
}
let mut parts = segment.text.split('\n').peekable();
while let Some(part) = parts.next() {
if !part.is_empty() {
current.push(Segment::new(part, segment.style.clone()));
}
if parts.peek().is_some() {
// The break between parts closes the current line.
lines.push(std::mem::take(&mut current));
}
}
}
if !current.is_empty() {
lines.push(current);
}
lines
}
/// Shape a set of lines into exactly `height` rows of `width` cells: crop
/// extra rows, pad each row to `width`, and append blank rows to reach
/// `height`. Port of `Segment.set_shape` (`style=None`, `new_lines=False`).
pub fn set_shape(lines: Vec<Vec<Segment>>, width: usize, height: usize) -> Vec<Vec<Segment>> {
let mut shaped: Vec<Vec<Segment>> = lines
.into_iter()
.take(height)
.map(|line| Segment::adjust_line_length(&line, width, None))
.collect();
while shaped.len() < height {
shaped.push(vec![Segment::new(" ".repeat(width), None)]);
}
shaped
}
/// Crop every line in a segment stream to at most `width` cells, discarding
/// the excess and leaving short lines alone.
///
/// Port of `Segment.split_and_crop_lines` with `pad=False`, which is what
/// `Console.print(crop=True)` applies to the finished stream. It is the only
/// thing standing between an [`Overflow::Ignore`](crate::console::Overflow)
/// text and a line that runs off the side of the terminal.
///
/// Control segments occupy no cells and are always kept, so cursor moves and
/// hyperlink codes survive a crop.
pub fn crop_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
let mut result: Vec<Segment> = Vec::with_capacity(segments.len());
let mut used = 0usize;
for segment in segments {
if segment.control {
result.push(segment.clone());
continue;
}
if segment.text == "\n" {
used = 0;
result.push(segment.clone());
continue;
}
let length = segment.cell_length();
if used + length <= width {
used += length;
result.push(segment.clone());
} else if used < width {
// Straddles the crop: keep the part that fits. A wide character
// across the boundary is dropped and the gap padded, as
// `set_cell_size` does everywhere else.
result.push(Segment::new(
crate::cells::set_cell_size(&segment.text, width - used),
segment.style.clone(),
));
used = width;
}
// Anything else is wholly past the crop, so it is dropped.
}
result
}
/// Pad (with a styled space run) or crop a single line to exactly `length`
/// cells. Port of `Segment.adjust_line_length`.
pub fn adjust_line_length(
line: &[Segment],
length: usize,
style: Option<Style>,
) -> Vec<Segment> {
let line_length: usize = line.iter().map(Segment::cell_length).sum();
if line_length == length {
line.to_vec()
} else if line_length < length {
let mut new_line = line.to_vec();
new_line.push(Segment::new(" ".repeat(length - line_length), style));
new_line
} else {
// Crop from the left, honoring cell widths.
let mut new_line: Vec<Segment> = Vec::new();
let mut remaining = length;
for segment in line {
let seg_len = segment.cell_length();
if seg_len <= remaining {
new_line.push(segment.clone());
remaining -= seg_len;
} else {
let cropped = crate::cells::set_cell_size(&segment.text, remaining);
new_line.push(Segment::new(cropped, segment.style.clone()));
break;
}
}
new_line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Cropping is per line, leaves short lines alone, and keeps zero-width
/// control segments so cursor moves survive.
#[test]
fn crop_lines_cuts_each_line_independently() {
let segments = vec![
Segment::new("hello world", None),
Segment::line(),
Segment::new("hi", None),
Segment::line(),
Segment::control("\x1b[2A"),
Segment::new("abcdefgh", None),
];
let cropped = Segment::crop_lines(&segments, 5);
let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
assert_eq!(texts, vec!["hello", "\n", "hi", "\n", "\x1b[2A", "abcde"]);
}
/// A wide character straddling the crop is dropped whole and its cell padded,
/// so the line still occupies exactly the requested width.
#[test]
fn crop_lines_pads_a_split_wide_character() {
let segments = vec![Segment::new("aa你好", None)];
let cropped = Segment::crop_lines(&segments, 5);
assert_eq!(cropped[0].text, "aa你 ");
}
/// A crop boundary falling between segments keeps the styles of the ones it
/// kept and drops the rest entirely.
#[test]
fn crop_lines_preserves_styles_and_drops_the_tail() {
let bold = Style::parse("bold").unwrap();
let segments = vec![
Segment::new("abc", Some(bold.clone())),
Segment::new("defgh", None),
];
let cropped = Segment::crop_lines(&segments, 3);
assert_eq!(cropped.len(), 1);
assert_eq!(cropped[0].text, "abc");
assert_eq!(cropped[0].style, Some(bold));
}
#[test]
fn cell_length_ignores_control() {
assert_eq!(Segment::new("abc", None).cell_length(), 3);
assert_eq!(Segment::control("\x1b[2J").cell_length(), 0);
}
}