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 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 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 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 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 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 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 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 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 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 out.push(Segment::line());
272 used = 0;
273 continue;
274 }
275 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 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 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 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 #[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 #[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 #[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 let fits = Segment::crop_lines(&segments[..2], 3);
438 assert_eq!(fits.len(), 2);
439 }
440
441 #[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 #[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 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 #[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 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 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 #[test]
536 fn fold_lines_words_breaks_between_words() {
537 let segments = vec![Segment::new("the quick brown fox", None)];
538 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 #[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 #[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 #[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 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}