1use crate::style::Style;
4use unicode_segmentation::UnicodeSegmentation;
5use unicode_width::UnicodeWidthStr;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Segment {
12 pub text: String,
14 pub style: Style,
16 pub is_control: bool,
18}
19
20impl Segment {
21 pub fn new(text: impl Into<String>) -> Self {
23 Self {
24 text: text.into(),
25 style: Style::default(),
26 is_control: false,
27 }
28 }
29
30 pub fn styled(text: impl Into<String>, style: Style) -> Self {
32 Self {
33 text: text.into(),
34 style,
35 is_control: false,
36 }
37 }
38
39 pub fn control(text: impl Into<String>) -> Self {
41 Self {
42 text: text.into(),
43 style: Style::default(),
44 is_control: true,
45 }
46 }
47
48 pub fn blank(width: u16) -> Self {
50 Self {
51 text: " ".repeat(width as usize),
52 style: Style::default(),
53 is_control: false,
54 }
55 }
56
57 pub fn width(&self) -> usize {
59 if self.is_control {
60 return 0;
61 }
62 UnicodeWidthStr::width(self.text.as_str())
63 }
64
65 pub fn display_width(&self) -> usize {
67 self.width()
68 }
69
70 pub fn is_empty(&self) -> bool {
72 self.text.is_empty()
73 }
74
75 pub fn grapheme_widths(&self) -> Vec<(String, usize)> {
80 if self.is_control {
81 return Vec::new();
82 }
83 self.text
84 .graphemes(true)
85 .map(|g| (g.to_string(), UnicodeWidthStr::width(g)))
86 .collect()
87 }
88
89 pub fn char_count(&self) -> usize {
94 if self.is_control {
95 return 0;
96 }
97 self.text.graphemes(true).count()
98 }
99
100 pub fn truncate_to_width(&self, max_width: usize) -> Segment {
106 self.split_at(max_width).0
107 }
108
109 pub fn pad_to_width(&self, target_width: usize) -> Segment {
113 let current = self.width();
114 if current >= target_width {
115 return self.clone();
116 }
117 let padding = target_width - current;
118 let mut text = self.text.clone();
119 for _ in 0..padding {
120 text.push(' ');
121 }
122 Segment::styled(text, self.style.clone())
123 }
124
125 pub fn split_at(&self, offset: usize) -> (Segment, Segment) {
135 if offset == 0 {
136 return (
137 Segment::styled(String::new(), self.style.clone()),
138 self.clone(),
139 );
140 }
141 if offset >= self.width() {
142 return (
143 self.clone(),
144 Segment::styled(String::new(), self.style.clone()),
145 );
146 }
147
148 let graphemes: Vec<(&str, usize)> = self
150 .text
151 .graphemes(true)
152 .map(|g| (g, UnicodeWidthStr::width(g)))
153 .collect();
154
155 let mut left = String::new();
156 let mut current_width = 0;
157 let mut split_idx = 0; let mut need_left_pad = false;
159
160 for (i, &(grapheme, gw)) in graphemes.iter().enumerate() {
161 if current_width + gw > offset {
162 if current_width < offset && gw > 1 {
164 left.push(' ');
166 need_left_pad = true;
167 }
168 split_idx = i;
169 break;
170 }
171 left.push_str(grapheme);
172 current_width += gw;
173 if current_width == offset {
174 let mut j = i + 1;
177 while j < graphemes.len() && graphemes[j].1 == 0 {
178 left.push_str(graphemes[j].0);
179 j += 1;
180 }
181 split_idx = j;
182 break;
183 }
184 }
185
186 let mut right = String::new();
188 if need_left_pad {
189 right.push(' ');
191 for &(grapheme, _) in &graphemes[split_idx + 1..] {
193 right.push_str(grapheme);
194 }
195 } else {
196 for &(grapheme, _) in &graphemes[split_idx..] {
197 right.push_str(grapheme);
198 }
199 }
200
201 (
202 Segment::styled(left, self.style.clone()),
203 Segment::styled(right, self.style.clone()),
204 )
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn ascii_width() {
214 assert_eq!(Segment::new("hello").width(), 5);
215 }
216
217 #[test]
218 fn empty_width() {
219 assert_eq!(Segment::new("").width(), 0);
220 }
221
222 #[test]
223 fn control_width_is_zero() {
224 assert_eq!(Segment::control("ESC[1m").width(), 0);
225 }
226
227 #[test]
228 fn cjk_width() {
229 assert_eq!(Segment::new("\u{4e16}\u{754c}").width(), 4); }
232
233 #[test]
234 fn split_ascii() {
235 let s = Segment::new("hello");
236 let (l, r) = s.split_at(3);
237 assert_eq!(l.text, "hel");
238 assert_eq!(r.text, "lo");
239 }
240
241 #[test]
242 fn split_at_zero() {
243 let s = Segment::new("hello");
244 let (l, r) = s.split_at(0);
245 assert_eq!(l.text, "");
246 assert_eq!(r.text, "hello");
247 }
248
249 #[test]
250 fn split_at_end() {
251 let s = Segment::new("hello");
252 let (l, r) = s.split_at(5);
253 assert_eq!(l.text, "hello");
254 assert_eq!(r.text, "");
255 }
256
257 #[test]
258 fn split_beyond_end() {
259 let s = Segment::new("hi");
260 let (l, r) = s.split_at(100);
261 assert_eq!(l.text, "hi");
262 assert_eq!(r.text, "");
263 }
264
265 #[test]
266 fn is_empty() {
267 assert!(Segment::new("").is_empty());
268 assert!(!Segment::new("x").is_empty());
269 }
270
271 #[test]
272 fn styled_preserves_style_on_split() {
273 let s = Segment::styled("hello", Style::new().bold(true));
274 let (l, r) = s.split_at(2);
275 assert!(l.style.bold);
276 assert!(r.style.bold);
277 }
278
279 #[test]
282 fn emoji_width_is_two() {
283 let s = Segment::new("\u{1f600}"); assert_eq!(s.width(), 2);
286 }
287
288 #[test]
289 fn emoji_at_split_boundary() {
290 let s = Segment::new("A\u{1f600}B");
292 assert_eq!(s.width(), 4);
293
294 let (l, r) = s.split_at(1);
296 assert_eq!(l.text, "A");
297 assert_eq!(r.text, "\u{1f600}B");
298
299 let (l2, r2) = s.split_at(2);
302 assert_eq!(l2.text, "A ");
304 assert_eq!(l2.width(), 2);
305 assert_eq!(r2.text, " B");
307 }
308
309 #[test]
310 fn combining_diacritics_width() {
311 let s = Segment::new("e\u{0301}"); assert_eq!(s.width(), 1);
315 assert_eq!(s.char_count(), 1);
316 }
317
318 #[test]
319 fn mixed_ascii_emoji_cjk() {
320 let s = Segment::new("Hi\u{1f600}\u{4e16}");
322 assert_eq!(s.width(), 6);
323 assert_eq!(s.char_count(), 4); }
325
326 #[test]
327 fn grapheme_widths_returns_correct_values() {
328 let s = Segment::new("A\u{4e16}B");
329 let widths = s.grapheme_widths();
330 assert_eq!(widths.len(), 3);
331 assert_eq!(widths[0], ("A".to_string(), 1));
332 assert_eq!(widths[1], ("\u{4e16}".to_string(), 2));
333 assert_eq!(widths[2], ("B".to_string(), 1));
334 }
335
336 #[test]
337 fn char_count_returns_grapheme_cluster_count() {
338 assert_eq!(Segment::new("Hello").char_count(), 5);
340 assert_eq!(Segment::new("").char_count(), 0);
342 assert_eq!(Segment::new("\u{4e16}\u{754c}").char_count(), 2);
344 assert_eq!(Segment::control("ESC").char_count(), 0);
346 }
347
348 #[test]
349 fn split_preserves_combining_marks() {
350 let s = Segment::new("ae\u{0301}b");
352 assert_eq!(s.width(), 3);
353 assert_eq!(s.char_count(), 3);
354
355 let (l, r) = s.split_at(1);
357 assert_eq!(l.text, "a");
358 assert_eq!(r.text, "e\u{0301}b");
360
361 let (l2, r2) = s.split_at(2);
363 assert_eq!(l2.text, "ae\u{0301}");
364 assert_eq!(r2.text, "b");
365 }
366
367 #[test]
368 fn empty_segment_grapheme_operations() {
369 let s = Segment::new("");
370 assert_eq!(s.grapheme_widths().len(), 0);
371 assert_eq!(s.char_count(), 0);
372 let (l, r) = s.split_at(0);
373 assert_eq!(l.text, "");
374 assert_eq!(r.text, "");
375 }
376
377 #[test]
378 fn grapheme_widths_empty_for_control() {
379 let s = Segment::control("\x1b[1m");
380 assert!(s.grapheme_widths().is_empty());
381 }
382
383 #[test]
386 fn truncate_to_width_ascii_exact_fit() {
387 let s = Segment::new("hello");
388 let truncated = s.truncate_to_width(5);
389 assert_eq!(truncated.text, "hello");
390 assert_eq!(truncated.width(), 5);
391 }
392
393 #[test]
394 fn truncate_to_width_cuts_before_wide_char_at_boundary() {
395 let s = Segment::new("A\u{4e16}B");
397 assert_eq!(s.width(), 4);
398 let truncated = s.truncate_to_width(2);
401 assert_eq!(truncated.width(), 2);
402 assert_eq!(truncated.text, "A ");
403 }
404
405 #[test]
406 fn truncate_to_width_zero_gives_empty() {
407 let s = Segment::new("hello");
408 let truncated = s.truncate_to_width(0);
409 assert_eq!(truncated.text, "");
410 assert_eq!(truncated.width(), 0);
411 }
412
413 #[test]
414 fn truncate_to_width_beyond_length_unchanged() {
415 let s = Segment::new("hi");
416 let truncated = s.truncate_to_width(100);
417 assert_eq!(truncated.text, "hi");
418 assert_eq!(truncated.width(), 2);
419 }
420
421 #[test]
422 fn pad_to_width_adds_trailing_spaces() {
423 let s = Segment::new("AB");
424 let padded = s.pad_to_width(5);
425 assert_eq!(padded.text, "AB ");
426 assert_eq!(padded.width(), 5);
427 }
428
429 #[test]
430 fn pad_to_width_already_at_target_unchanged() {
431 let s = Segment::new("hello");
432 let padded = s.pad_to_width(5);
433 assert_eq!(padded.text, "hello");
434 }
435
436 #[test]
437 fn pad_to_width_already_wider_unchanged() {
438 let s = Segment::new("hello world");
439 let padded = s.pad_to_width(5);
440 assert_eq!(padded.text, "hello world");
441 }
442
443 #[test]
444 fn style_preserved_through_truncation_and_padding() {
445 let style = Style::new().bold(true);
446 let s = Segment::styled("hello world", style.clone());
447
448 let truncated = s.truncate_to_width(5);
449 assert!(truncated.style.bold);
450 assert_eq!(truncated.style, style);
451
452 let padded = s.pad_to_width(20);
453 assert!(padded.style.bold);
454 assert_eq!(padded.style, style);
455 }
456
457 #[test]
460 fn zwj_family_emoji_width() {
461 let s = Segment::new("\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}");
463 assert_eq!(s.width(), 2);
465 }
466
467 #[test]
468 fn zwj_family_emoji_grapheme_widths() {
469 let s = Segment::new("\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}");
470 let widths = s.grapheme_widths();
471 assert_eq!(widths.len(), 1);
473 assert_eq!(widths[0].1, 2);
475 }
476
477 #[test]
478 fn flag_emoji_width() {
479 let s = Segment::new("\u{1F1FA}\u{1F1F8}");
481 assert_eq!(s.width(), 2);
482 }
483
484 #[test]
485 fn skin_tone_emoji_width() {
486 let s = Segment::new("\u{1F44D}\u{1F3FD}");
488 assert_eq!(s.width(), 2);
489 }
490
491 #[test]
492 fn split_segment_at_zwj_emoji_boundary() {
493 let s = Segment::new("A\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}B");
495 assert_eq!(s.width(), 4);
496
497 let (l, r) = s.split_at(1);
499 assert_eq!(l.text, "A");
500 assert_eq!(l.width(), 1);
501 assert_eq!(r.width(), 3); }
504
505 #[test]
506 fn char_count_with_complex_emoji() {
507 let s = Segment::new("\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}");
509 assert_eq!(s.char_count(), 1);
510 }
511
512 #[test]
513 fn mixed_ascii_zwj_emoji_cjk() {
514 let s = Segment::new("Hi\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{4e16}!");
516 assert_eq!(s.width(), 7);
517 assert_eq!(s.char_count(), 5); }
519
520 #[test]
521 fn keycap_sequence_handling() {
522 let s = Segment::new("#\u{FE0F}\u{20E3}");
524 assert_eq!(s.char_count(), 1);
526 let w = s.width();
528 assert!((1..=2).contains(&w));
529 }
530}