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>> {
121 let mut lines: Vec<Vec<Segment>> = Vec::new();
122 let mut current: Vec<Segment> = Vec::new();
123 for segment in segments {
124 if segment.control || !segment.text.contains('\n') {
125 if !segment.text.is_empty() {
126 current.push(segment.clone());
127 }
128 continue;
129 }
130 let mut parts = segment.text.split('\n').peekable();
131 while let Some(part) = parts.next() {
132 if !part.is_empty() {
133 current.push(Segment::new(part, segment.style.clone()));
134 }
135 if parts.peek().is_some() {
136 lines.push(std::mem::take(&mut current));
138 }
139 }
140 }
141 if !current.is_empty() {
142 lines.push(current);
143 }
144 lines
145 }
146
147 pub fn set_shape(lines: Vec<Vec<Segment>>, width: usize, height: usize) -> Vec<Vec<Segment>> {
151 let mut shaped: Vec<Vec<Segment>> = lines
152 .into_iter()
153 .take(height)
154 .map(|line| Segment::adjust_line_length(&line, width, None))
155 .collect();
156 while shaped.len() < height {
157 shaped.push(vec![Segment::new(" ".repeat(width), None)]);
158 }
159 shaped
160 }
161
162 pub fn fold_lines_words(segments: &[Segment], width: usize) -> Vec<Segment> {
175 if width == 0 {
176 return segments.to_vec();
177 }
178 let mut out = Vec::new();
179 let lines = Self::split_lines(segments);
180 let last = lines.len().saturating_sub(1);
181 for (index, line) in lines.into_iter().enumerate() {
182 let plain: String = line
183 .iter()
184 .filter(|segment| !segment.control)
185 .map(|segment| segment.text.as_str())
186 .collect();
187 let breaks = crate::wrap::divide_line(&plain, width, true);
188
189 let mut char_pos = 0usize;
190 let mut next_break = 0usize;
191 for segment in line {
192 if segment.control {
193 out.push(segment);
194 continue;
195 }
196 let mut buf = String::new();
197 for ch in segment.text.chars() {
198 while next_break < breaks.len() && char_pos == breaks[next_break] {
199 if !buf.is_empty() {
200 out.push(Segment::new(buf.clone(), segment.style.clone()));
201 buf.clear();
202 }
203 out.push(Segment::line());
204 next_break += 1;
205 }
206 buf.push(ch);
207 char_pos += 1;
208 }
209 if !buf.is_empty() {
210 out.push(Segment::new(buf, segment.style.clone()));
211 }
212 }
213 if index != last {
214 out.push(Segment::line());
215 }
216 }
217 out
218 }
219
220 pub fn fold_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
235 if width == 0 {
236 return segments.to_vec();
237 }
238 let mut out = Vec::new();
239 let lines = Self::split_lines(segments);
240 let last = lines.len().saturating_sub(1);
241 for (index, line) in lines.into_iter().enumerate() {
242 let mut used = 0usize;
243 for segment in line {
244 if segment.control {
245 out.push(segment);
246 continue;
247 }
248 let mut remaining = segment.text.as_str();
251 while !remaining.is_empty() {
252 let room = width.saturating_sub(used);
253 if room == 0 {
254 out.push(Segment::line());
255 used = 0;
256 continue;
257 }
258 let chunks = crate::cells::chop_cells(remaining, room);
259 let mut head = chunks.first().cloned().unwrap_or_default();
260 if head.is_empty() {
261 if used > 0 {
262 out.push(Segment::line());
265 used = 0;
266 continue;
267 }
268 let (spans, _) = crate::cells::split_graphemes(remaining);
283 let take = spans.first().map_or(remaining.len(), |span| span.1);
284 head = remaining[..take].to_string();
285 }
286 used += crate::cells::cell_len(&head);
287 remaining = &remaining[head.len()..];
288 out.push(Segment::new(head, segment.style.clone()));
289 if !remaining.is_empty() {
290 out.push(Segment::line());
291 used = 0;
292 }
293 }
294 }
295 if index != last {
296 out.push(Segment::line());
297 }
298 }
299 out
300 }
301
302 pub fn crop_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
313 let mut result: Vec<Segment> = Vec::with_capacity(segments.len());
314 let mut used = 0usize;
315 for segment in segments {
316 if segment.control {
317 result.push(segment.clone());
318 continue;
319 }
320 if segment.text == "\n" {
321 used = 0;
322 result.push(segment.clone());
323 continue;
324 }
325 let length = segment.cell_length();
326 if used + length <= width {
327 used += length;
328 result.push(segment.clone());
329 } else if used < width {
330 result.push(Segment::new(
334 crate::cells::set_cell_size(&segment.text, width - used),
335 segment.style.clone(),
336 ));
337 used = width;
338 }
339 }
341 result
342 }
343
344 pub fn adjust_line_length(
347 line: &[Segment],
348 length: usize,
349 style: Option<Style>,
350 ) -> Vec<Segment> {
351 let line_length: usize = line.iter().map(Segment::cell_length).sum();
352 if line_length == length {
353 line.to_vec()
354 } else if line_length < length {
355 let mut new_line = line.to_vec();
356 new_line.push(Segment::new(" ".repeat(length - line_length), style));
357 new_line
358 } else {
359 let mut new_line: Vec<Segment> = Vec::new();
361 let mut remaining = length;
362 for segment in line {
363 let seg_len = segment.cell_length();
364 if seg_len <= remaining {
365 new_line.push(segment.clone());
366 remaining -= seg_len;
367 } else {
368 let cropped = crate::cells::set_cell_size(&segment.text, remaining);
369 new_line.push(Segment::new(cropped, segment.style.clone()));
370 break;
371 }
372 }
373 new_line
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
385 fn crop_lines_cuts_each_line_independently() {
386 let segments = vec![
387 Segment::new("hello world", None),
388 Segment::line(),
389 Segment::new("hi", None),
390 Segment::line(),
391 Segment::control("\x1b[2A"),
392 Segment::new("abcdefgh", None),
393 ];
394 let cropped = Segment::crop_lines(&segments, 5);
395 let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
396 assert_eq!(texts, vec!["hello", "\n", "hi", "\n", "\x1b[2A", "abcde"]);
397 }
398
399 #[test]
402 fn crop_lines_pads_a_split_wide_character() {
403 let segments = vec![Segment::new("aa你好", None)];
404 let cropped = Segment::crop_lines(&segments, 5);
405 assert_eq!(cropped[0].text, "aa你 ");
406 }
407
408 #[test]
411 fn crop_lines_preserves_styles_and_drops_the_tail() {
412 let bold = Style::parse("bold").unwrap();
413 let segments = vec![
414 Segment::new("abc", Some(bold.clone())),
415 Segment::new("defgh", None),
416 ];
417 let cropped = Segment::crop_lines(&segments, 3);
418 assert_eq!(cropped.len(), 1);
419 assert_eq!(cropped[0].text, "abc");
420 assert_eq!(cropped[0].style, Some(bold));
421 }
422
423 #[test]
424 fn cell_length_ignores_control() {
425 assert_eq!(Segment::new("abc", None).cell_length(), 3);
426 assert_eq!(Segment::control("\x1b[2J").cell_length(), 0);
427 }
428
429 #[test]
430 fn fold_lines_carries_the_overflow_instead_of_dropping_it() {
431 let segments = vec![Segment::new("abcdefghij", None)];
432 let folded = Segment::fold_lines(&segments, 4);
433 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
434 assert_eq!(text.replace('\n', ""), "abcdefghij");
436 assert_eq!(Segment::split_lines(&folded).len(), 3);
437 }
438
439 #[test]
440 fn fold_lines_preserves_styles_across_a_break() {
441 let style = Style::parse("bold").expect("valid style");
442 let segments = vec![Segment::new("abcdef", Some(style.clone()))];
443 let folded = Segment::fold_lines(&segments, 3);
444 for segment in folded.iter().filter(|s| !s.text.contains('\n')) {
445 assert_eq!(segment.style.as_ref(), Some(&style), "style lost on fold");
446 }
447 }
448
449 #[test]
454 fn fold_lines_never_splits_a_grapheme() {
455 let heart = "\u{2764}\u{fe0f}";
456 let segments = vec![Segment::new(heart.repeat(3), None)];
457 let folded = Segment::fold_lines(&segments, 1);
458 let rows: Vec<String> = Segment::split_lines(&folded)
459 .iter()
460 .map(|line| line.iter().map(|s| s.text.as_str()).collect())
461 .collect();
462 assert_eq!(rows, vec![heart, heart, heart]);
463 }
464
465 #[test]
466 fn crop_lines_still_drops_the_overflow() {
467 let segments = vec![Segment::new("abcdefghij", None)];
470 let cropped = Segment::crop_lines(&segments, 4);
471 let text: String = cropped.iter().map(|s| s.text.as_str()).collect();
472 assert_eq!(text, "abcd");
473 }
474
475 #[test]
476 fn fold_lines_terminates_when_a_glyph_is_wider_than_the_width() {
477 let segments = vec![Segment::new("\u{4f60}\u{4f60}", None)];
481 let folded = Segment::fold_lines(&segments, 1);
482 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
483 assert_eq!(
484 text.matches('\u{4f60}').count(),
485 2,
486 "both characters should survive, overflowing rather than looping"
487 );
488 }
489
490 #[test]
494 fn fold_lines_words_breaks_between_words() {
495 let segments = vec![Segment::new("the quick brown fox", None)];
496 let folded = Segment::fold_lines_words(&segments, 12);
499 let lines: Vec<String> = Segment::split_lines(&folded)
500 .iter()
501 .map(|line| line.iter().map(|s| s.text.as_str()).collect())
502 .collect();
503 assert_eq!(lines, vec!["the quick ", "brown fox"]);
504 }
505
506 #[test]
509 fn fold_lines_words_preserves_styles_across_a_break() {
510 let green = Style::parse("green").expect("valid style");
511 let segments = vec![
512 Segment::new("key: ", None),
513 Segment::new("alpha beta gamma", Some(green.clone())),
514 ];
515 let folded = Segment::fold_lines_words(&segments, 12);
516 let styled: String = folded
517 .iter()
518 .filter(|s| s.style.as_ref() == Some(&green))
519 .map(|s| s.text.as_str())
520 .collect();
521 assert_eq!(styled, "alpha beta gamma", "style lost across the break");
522 }
523
524 #[test]
527 fn fold_lines_words_keeps_every_character() {
528 let segments = vec![
529 Segment::new("short ", None),
530 Segment::new("z".repeat(25), None),
531 Segment::new(" tail", None),
532 ];
533 for width in 1..=30 {
534 let folded = Segment::fold_lines_words(&segments, width);
535 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
536 assert_eq!(
537 text.replace('\n', ""),
538 format!("short {} tail", "z".repeat(25)),
539 "width {width} lost or reordered characters"
540 );
541 }
542 }
543
544 #[test]
547 fn fold_lines_words_keeps_control_segments() {
548 let segments = vec![
549 Segment::control("\x1b]8;;http://x\x1b\\"),
550 Segment::new("alpha beta", None),
551 ];
552 let folded = Segment::fold_lines_words(&segments, 8);
553 assert_eq!(folded.iter().filter(|s| s.control).count(), 1);
554 let text: String = folded
555 .iter()
556 .filter(|s| !s.control)
557 .map(|s| s.text.as_str())
558 .collect();
559 assert_eq!(text, "alpha \nbeta");
560 }
561
562 #[test]
563 fn fold_lines_terminates_at_every_narrow_width() {
564 let sample = "a\u{4f60}b\u{1f600}c";
566 for width in 1..=6 {
567 let folded = Segment::fold_lines(&[Segment::new(sample, None)], width);
568 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
569 assert!(
570 text.contains('c'),
571 "width {width} lost the tail, or did not terminate"
572 );
573 }
574 }
575}