1use crate::cells::cell_len;
11use crate::style::Style;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Segment {
16 pub text: String,
17 pub style: Option<Style>,
18 pub control: bool,
20}
21
22impl Segment {
23 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 pub fn line() -> Self {
34 Segment {
35 text: "\n".to_string(),
36 style: None,
37 control: false,
38 }
39 }
40
41 pub fn control(text: impl Into<String>) -> Self {
43 Segment {
44 text: text.into(),
45 style: None,
46 control: true,
47 }
48 }
49
50 pub fn cell_length(&self) -> usize {
52 if self.control {
53 0
54 } else {
55 cell_len(&self.text)
56 }
57 }
58
59 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 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 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 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 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 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 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 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 out.push(Segment::line());
252 used = 0;
253 continue;
254 }
255 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 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 result.push(Segment::new(
321 crate::cells::set_cell_size(&segment.text, width - used),
322 segment.style.clone(),
323 ));
324 used = width;
325 }
326 }
328 result
329 }
330
331 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 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 #[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 #[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 #[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 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 #[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 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 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 #[test]
481 fn fold_lines_words_breaks_between_words() {
482 let segments = vec![Segment::new("the quick brown fox", None)];
483 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 #[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 #[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 #[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 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}