1use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Color {
8 Black,
9 Red,
10 Green,
11 Yellow,
12 Blue,
13 Magenta,
14 Cyan,
15 White,
16 BrightBlack,
17 BrightRed,
18 BrightGreen,
19 BrightYellow,
20 BrightBlue,
21 BrightMagenta,
22 BrightCyan,
23 BrightWhite,
24 Ansi256(u8),
25 Rgb(u8, u8, u8),
26}
27
28impl Color {
29 pub fn fg_ansi(&self) -> String {
30 self.fg_code()
31 }
32
33 pub fn bg_ansi(&self) -> String {
34 self.bg_code()
35 }
36
37 fn fg_code(&self) -> String {
38 match self {
39 Color::Black => "30".into(),
40 Color::Red => "31".into(),
41 Color::Green => "32".into(),
42 Color::Yellow => "33".into(),
43 Color::Blue => "34".into(),
44 Color::Magenta => "35".into(),
45 Color::Cyan => "36".into(),
46 Color::White => "37".into(),
47 Color::BrightBlack => "90".into(),
48 Color::BrightRed => "91".into(),
49 Color::BrightGreen => "92".into(),
50 Color::BrightYellow => "93".into(),
51 Color::BrightBlue => "94".into(),
52 Color::BrightMagenta => "95".into(),
53 Color::BrightCyan => "96".into(),
54 Color::BrightWhite => "97".into(),
55 Color::Ansi256(n) => format!("38;5;{}", n),
56 Color::Rgb(r, g, b) => format!("38;2;{};{};{}", r, g, b),
57 }
58 }
59
60 fn bg_code(&self) -> String {
61 match self {
62 Color::Black => "40".into(),
63 Color::Red => "41".into(),
64 Color::Green => "42".into(),
65 Color::Yellow => "43".into(),
66 Color::Blue => "44".into(),
67 Color::Magenta => "45".into(),
68 Color::Cyan => "46".into(),
69 Color::White => "47".into(),
70 Color::BrightBlack => "100".into(),
71 Color::BrightRed => "101".into(),
72 Color::BrightGreen => "102".into(),
73 Color::BrightYellow => "103".into(),
74 Color::BrightBlue => "104".into(),
75 Color::BrightMagenta => "105".into(),
76 Color::BrightCyan => "106".into(),
77 Color::BrightWhite => "107".into(),
78 Color::Ansi256(n) => format!("48;5;{}", n),
79 Color::Rgb(r, g, b) => format!("48;2;{};{};{}", r, g, b),
80 }
81 }
82
83 pub fn from_hex(hex: &str) -> Option<Self> {
85 let hex = hex.trim_start_matches('#');
86 if hex.len() != 6 {
87 return None;
88 }
89 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
90 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
91 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
92 Some(Color::Rgb(r, g, b))
93 }
94
95 pub fn lighten(&self, amount: f64) -> Self {
97 match self {
98 Color::Rgb(r, g, b) => {
99 let factor = normalized_color_amount(amount);
100 Color::Rgb(
101 (*r as f64 + (255.0 - *r as f64) * factor) as u8,
102 (*g as f64 + (255.0 - *g as f64) * factor) as u8,
103 (*b as f64 + (255.0 - *b as f64) * factor) as u8,
104 )
105 }
106 other => *other,
107 }
108 }
109
110 pub fn darken(&self, amount: f64) -> Self {
112 match self {
113 Color::Rgb(r, g, b) => {
114 let factor = 1.0 - normalized_color_amount(amount);
115 Color::Rgb(
116 (*r as f64 * factor) as u8,
117 (*g as f64 * factor) as u8,
118 (*b as f64 * factor) as u8,
119 )
120 }
121 other => *other,
122 }
123 }
124
125 pub fn gray(level: u8) -> Self {
127 Color::Rgb(level, level, level)
128 }
129
130 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
132 Color::Rgb(r, g, b)
133 }
134
135 pub fn ansi(n: u8) -> Self {
137 Color::Ansi256(n)
138 }
139}
140
141fn normalized_color_amount(amount: f64) -> f64 {
142 if amount.is_finite() {
143 amount.clamp(0.0, 1.0)
144 } else {
145 0.0
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum Border {
151 None,
152 Single,
153 Double,
154 Rounded,
155 Thick,
156}
157
158impl Border {
159 fn chars(&self) -> Option<BorderChars> {
160 match self {
161 Border::None => None,
162 Border::Single => Some(BorderChars {
163 tl: '┌',
164 tr: '┐',
165 bl: '└',
166 br: '┘',
167 h: '─',
168 v: '│',
169 }),
170 Border::Double => Some(BorderChars {
171 tl: '╔',
172 tr: '╗',
173 bl: '╚',
174 br: '╝',
175 h: '═',
176 v: '║',
177 }),
178 Border::Rounded => Some(BorderChars {
179 tl: '╭',
180 tr: '╮',
181 bl: '╰',
182 br: '╯',
183 h: '─',
184 v: '│',
185 }),
186 Border::Thick => Some(BorderChars {
187 tl: '┏',
188 tr: '┓',
189 bl: '┗',
190 br: '┛',
191 h: '━',
192 v: '┃',
193 }),
194 }
195 }
196}
197
198struct BorderChars {
199 tl: char,
200 tr: char,
201 bl: char,
202 br: char,
203 h: char,
204 v: char,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum Align {
209 Left,
210 Center,
211 Right,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct Style {
216 fg: Option<Color>,
217 bg: Option<Color>,
218 bold: bool,
219 italic: bool,
220 underline: bool,
221 strikethrough: bool,
222 dim: bool,
223 reverse: bool,
224 padding: [u16; 4],
225 margin: [u16; 4],
226 border: Border,
227 border_fg: Option<Color>,
228 width: Option<u16>,
229 height: Option<u16>,
230 align: Align,
231}
232
233impl Default for Style {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241impl Style {
242 pub fn new() -> Self {
243 Self {
244 fg: None,
245 bg: None,
246 bold: false,
247 italic: false,
248 underline: false,
249 strikethrough: false,
250 dim: false,
251 reverse: false,
252 padding: [0; 4],
253 margin: [0; 4],
254 border: Border::None,
255 border_fg: None,
256 width: None,
257 height: None,
258 align: Align::Left,
259 }
260 }
261
262 pub fn fg(mut self, color: Color) -> Self {
263 self.fg = Some(color);
264 self
265 }
266 pub fn bg(mut self, color: Color) -> Self {
267 self.bg = Some(color);
268 self
269 }
270 pub fn bold(mut self) -> Self {
271 self.bold = true;
272 self
273 }
274 pub fn italic(mut self) -> Self {
275 self.italic = true;
276 self
277 }
278 pub fn underline(mut self) -> Self {
279 self.underline = true;
280 self
281 }
282 pub fn strikethrough(mut self) -> Self {
283 self.strikethrough = true;
284 self
285 }
286 pub fn dim(mut self) -> Self {
287 self.dim = true;
288 self
289 }
290 pub fn reverse(mut self) -> Self {
291 self.reverse = true;
292 self
293 }
294
295 pub fn foreground(&self) -> Option<Color> {
296 self.fg
297 }
298
299 pub fn background(&self) -> Option<Color> {
300 self.bg
301 }
302
303 pub fn is_bold(&self) -> bool {
304 self.bold
305 }
306
307 pub fn is_italic(&self) -> bool {
308 self.italic
309 }
310
311 pub fn is_underline(&self) -> bool {
312 self.underline
313 }
314
315 pub fn is_strikethrough(&self) -> bool {
316 self.strikethrough
317 }
318
319 pub fn is_dim(&self) -> bool {
320 self.dim
321 }
322
323 pub fn is_reverse(&self) -> bool {
324 self.reverse
325 }
326
327 pub fn padding_top(mut self, n: u16) -> Self {
328 self.padding[0] = n;
329 self
330 }
331 pub fn padding_right(mut self, n: u16) -> Self {
332 self.padding[1] = n;
333 self
334 }
335 pub fn padding_bottom(mut self, n: u16) -> Self {
336 self.padding[2] = n;
337 self
338 }
339 pub fn padding_left(mut self, n: u16) -> Self {
340 self.padding[3] = n;
341 self
342 }
343
344 pub fn padding(mut self, vertical: u16, horizontal: u16) -> Self {
345 self.padding = [vertical, horizontal, vertical, horizontal];
346 self
347 }
348
349 pub fn margin_top(mut self, n: u16) -> Self {
350 self.margin[0] = n;
351 self
352 }
353 pub fn margin_right(mut self, n: u16) -> Self {
354 self.margin[1] = n;
355 self
356 }
357 pub fn margin_bottom(mut self, n: u16) -> Self {
358 self.margin[2] = n;
359 self
360 }
361 pub fn margin_left(mut self, n: u16) -> Self {
362 self.margin[3] = n;
363 self
364 }
365
366 pub fn margin(mut self, vertical: u16, horizontal: u16) -> Self {
367 self.margin = [vertical, horizontal, vertical, horizontal];
368 self
369 }
370
371 pub fn border(mut self, border: Border) -> Self {
372 self.border = border;
373 self
374 }
375 pub fn border_fg(mut self, color: Color) -> Self {
376 self.border_fg = Some(color);
377 self
378 }
379
380 pub fn width(mut self, w: u16) -> Self {
381 self.width = Some(w);
382 self
383 }
384 pub fn height(mut self, h: u16) -> Self {
385 self.height = Some(h);
386 self
387 }
388 pub fn align(mut self, a: Align) -> Self {
389 self.align = a;
390 self
391 }
392
393 pub fn render(&self, content: &str) -> String {
394 let lines = split_lines_preserving_trailing_blank(content);
395 let content_height = self.content_height(lines.len());
396 let mut content_lines = lines
397 .iter()
398 .copied()
399 .take(content_height)
400 .collect::<Vec<_>>();
401 while content_lines.len() < content_height {
402 content_lines.push("");
403 }
404
405 let content_width = self.content_width(&content_lines);
406 let pad_left = self.padding[3] as usize;
407 let pad_right = self.padding[1] as usize;
408 let inner_width = content_width + pad_left + pad_right;
409
410 let border_chars = self.border.chars();
411
412 let mut result = Vec::new();
413
414 for _ in 0..self.margin[0] {
416 result.push(String::new());
417 }
418
419 let margin_left = " ".repeat(self.margin[3] as usize);
420 let margin_right = " ".repeat(self.margin[1] as usize);
421
422 if let Some(ref bc) = border_chars {
424 let border_line = format!(
425 "{}{}{}{}{}",
426 margin_left,
427 bc.tl,
428 str::repeat(&bc.h.to_string(), inner_width),
429 bc.tr,
430 margin_right
431 );
432 result.push(self.apply_border_style(&border_line));
433 }
434
435 for _ in 0..self.padding[0] {
437 result.push(self.apply_padding_line_style(
438 border_chars.as_ref(),
439 &margin_left,
440 inner_width,
441 &margin_right,
442 ));
443 }
444
445 for line in &content_lines {
447 let visible_width = visible_len(line);
448 let aligned = self.align_text(line, visible_width, content_width);
449 let padded = format!(
450 "{}{}{}",
451 " ".repeat(pad_left),
452 aligned,
453 " ".repeat(pad_right)
454 );
455
456 let styled_line = match &border_chars {
457 Some(bc) => self.apply_bordered_content_style(
458 &margin_left,
459 bc.v,
460 &padded,
461 bc.v,
462 &margin_right,
463 ),
464 None => {
465 let full_line = format!("{}{}{}", margin_left, padded, margin_right);
466 self.apply_text_style(&full_line)
467 }
468 };
469
470 result.push(styled_line);
471 }
472
473 for _ in 0..self.padding[2] {
475 result.push(self.apply_padding_line_style(
476 border_chars.as_ref(),
477 &margin_left,
478 inner_width,
479 &margin_right,
480 ));
481 }
482
483 if let Some(ref bc) = border_chars {
485 let border_line = format!(
486 "{}{}{}{}{}",
487 margin_left,
488 bc.bl,
489 str::repeat(&bc.h.to_string(), inner_width),
490 bc.br,
491 margin_right
492 );
493 result.push(self.apply_border_style(&border_line));
494 }
495
496 for _ in 0..self.margin[2] {
498 result.push(String::new());
499 }
500
501 if let Some(height) = self.height {
502 let height = height as usize;
503 result.truncate(height);
504 while result.len() < height {
505 result.push(String::new());
506 }
507 }
508
509 result.join("\n")
510 }
511
512 fn content_width(&self, lines: &[&str]) -> usize {
513 if let Some(w) = self.width {
514 let border_cost = if self.border != Border::None { 2 } else { 0 };
515 let pad_cost = self.padding[1] as usize + self.padding[3] as usize;
516 let margin_cost = self.margin[1] as usize + self.margin[3] as usize;
517 (w as usize).saturating_sub(border_cost + pad_cost + margin_cost)
518 } else {
519 lines.iter().map(|l| visible_len(l)).max().unwrap_or(0)
520 }
521 }
522
523 fn content_height(&self, line_count: usize) -> usize {
524 if let Some(h) = self.height {
525 let border_cost = if self.border != Border::None { 2 } else { 0 };
526 let pad_cost = self.padding[0] as usize + self.padding[2] as usize;
527 let margin_cost = self.margin[0] as usize + self.margin[2] as usize;
528 (h as usize).saturating_sub(border_cost + pad_cost + margin_cost)
529 } else {
530 line_count
531 }
532 }
533
534 fn align_text(&self, text: &str, text_width: usize, target_width: usize) -> String {
535 if text_width >= target_width {
536 return truncate_visible(text, target_width);
537 }
538 match self.align {
539 Align::Left => pad_visible(text, target_width),
540 Align::Right => right_visible(text, target_width),
541 Align::Center => center_visible(text, target_width),
542 }
543 }
544
545 fn apply_bordered_content_style(
546 &self,
547 margin_left: &str,
548 left_border: char,
549 content: &str,
550 right_border: char,
551 margin_right: &str,
552 ) -> String {
553 let left_border = left_border.to_string();
554 let right_border = right_border.to_string();
555
556 format!(
557 "{}{}{}{}{}",
558 self.apply_text_style_nonempty(margin_left),
559 self.apply_border_style(&left_border),
560 self.apply_text_style(content),
561 self.apply_border_style(&right_border),
562 self.apply_text_style_nonempty(margin_right)
563 )
564 }
565
566 fn apply_padding_line_style(
567 &self,
568 border_chars: Option<&BorderChars>,
569 margin_left: &str,
570 inner_width: usize,
571 margin_right: &str,
572 ) -> String {
573 let content = " ".repeat(inner_width);
574 match border_chars {
575 Some(bc) => {
576 self.apply_bordered_content_style(margin_left, bc.v, &content, bc.v, margin_right)
577 }
578 None => {
579 let line = format!("{margin_left}{content}{margin_right}");
580 self.apply_text_style(&line)
581 }
582 }
583 }
584
585 fn apply_text_style_nonempty(&self, text: &str) -> String {
586 if text.is_empty() {
587 String::new()
588 } else {
589 self.apply_text_style(text)
590 }
591 }
592
593 fn apply_text_style(&self, text: &str) -> String {
594 if text.is_empty() {
595 return String::new();
596 }
597
598 let mut codes = Vec::new();
599 if self.bold {
600 codes.push("1".to_string());
601 }
602 if self.dim {
603 codes.push("2".to_string());
604 }
605 if self.italic {
606 codes.push("3".to_string());
607 }
608 if self.underline {
609 codes.push("4".to_string());
610 }
611 if self.reverse {
612 codes.push("7".to_string());
613 }
614 if self.strikethrough {
615 codes.push("9".to_string());
616 }
617 if let Some(ref c) = self.fg {
618 codes.push(c.fg_code());
619 }
620 if let Some(ref c) = self.bg {
621 codes.push(c.bg_code());
622 }
623
624 if codes.is_empty() {
625 text.to_string()
626 } else {
627 format!("\x1b[{}m{}\x1b[0m", codes.join(";"), text)
628 }
629 }
630
631 fn apply_border_style(&self, text: &str) -> String {
632 let mut codes = Vec::new();
633 if let Some(c) = self.border_fg.or(self.fg) {
634 codes.push(c.fg_code());
635 }
636 if let Some(c) = self.bg {
637 codes.push(c.bg_code());
638 }
639
640 if codes.is_empty() {
641 text.to_string()
642 } else {
643 format!("\x1b[{}m{}\x1b[0m", codes.join(";"), text)
644 }
645 }
646}
647
648pub fn visible_len(s: &str) -> usize {
650 let stripped = strip_ansi(s);
651 UnicodeWidthStr::width(stripped.as_str())
652}
653
654pub fn truncate_visible(s: &str, width: usize) -> String {
656 if width == 0 {
657 return String::new();
658 }
659 if visible_len(s) <= width {
660 return s.to_string();
661 }
662 if width == 1 {
663 return "…".to_string();
664 }
665
666 let target = width - 1;
667 let mut out = String::new();
668 let mut used = 0;
669 let mut saw_sgr = false;
670 let mut hyperlink_open = false;
671 let mut index = 0usize;
672
673 while index < s.len() {
674 if let Some(end) = ansi_escape_sequence_end(s, index) {
675 let sequence = &s[index..end];
676 out.push_str(sequence);
677 if sequence.starts_with("\x1b[") {
678 saw_sgr = true;
679 }
680 if let Some(target) = osc8_link_target(sequence) {
681 hyperlink_open = !target.is_empty();
682 }
683 index = end;
684 continue;
685 }
686
687 let Some((end, cw)) = next_display_cell_boundary(s, index) else {
688 break;
689 };
690 if used + cw > target {
691 break;
692 }
693 out.push_str(&s[index..end]);
694 used += cw;
695 index = end;
696 }
697
698 out.push('…');
699 if saw_sgr {
700 out.push_str("\x1b[0m");
701 }
702 if hyperlink_open {
703 out.push_str("\x1b]8;;\x1b\\");
704 }
705 out
706}
707
708pub fn pad_visible(s: &str, width: usize) -> String {
712 let len = visible_len(s);
713 if len >= width {
714 s.to_string()
715 } else {
716 format!("{s}{}", " ".repeat(width - len))
717 }
718}
719
720pub fn repeat_visible_char(ch: char, width: usize) -> String {
725 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
726 if width == 0 {
727 return String::new();
728 }
729 if char_width == 0 {
730 return " ".repeat(width);
731 }
732
733 let count = width / char_width;
734 let remainder = width % char_width;
735 format!("{}{}", ch.to_string().repeat(count), " ".repeat(remainder))
736}
737
738pub fn repeat_visible(pattern: &str, width: usize) -> String {
744 if width == 0 {
745 return String::new();
746 }
747
748 let pattern = strip_ansi(pattern);
749 if UnicodeWidthStr::width(pattern.as_str()) == 0 {
750 return " ".repeat(width);
751 }
752
753 let mut out = String::new();
754 let mut used = 0usize;
755 while used < width {
756 let pass_start = used;
757 let mut index = 0usize;
758 while used < width {
759 let Some((end, cell_width)) = next_display_cell_boundary(&pattern, index) else {
760 break;
761 };
762 let cell = &pattern[index..end];
763 index = end;
764
765 if cell_width == 0 {
766 if !out.is_empty() {
767 out.push_str(cell);
768 }
769 continue;
770 }
771 if used + cell_width > width {
772 out.push_str(&" ".repeat(width - used));
773 return out;
774 }
775
776 out.push_str(cell);
777 used += cell_width;
778 }
779
780 if used == pass_start {
781 out.push_str(&" ".repeat(width - used));
782 return out;
783 }
784 }
785
786 out
787}
788
789pub fn fit_visible(s: &str, width: usize) -> String {
791 pad_visible(&truncate_visible(s, width), width)
792}
793
794pub fn right_visible(s: &str, width: usize) -> String {
796 let truncated = truncate_visible(s, width);
797 let len = visible_len(&truncated);
798 if len >= width {
799 truncated
800 } else {
801 format!("{}{truncated}", " ".repeat(width - len))
802 }
803}
804
805pub fn center_visible(s: &str, width: usize) -> String {
807 let truncated = truncate_visible(s, width);
808 let len = visible_len(&truncated);
809 if len >= width {
810 return truncated;
811 }
812 let left = (width - len) / 2;
813 let right = width - len - left;
814 format!("{}{}{}", " ".repeat(left), truncated, " ".repeat(right))
815}
816
817pub(crate) fn next_display_cell_boundary(value: &str, start: usize) -> Option<(usize, usize)> {
819 if start >= value.len() {
820 return None;
821 }
822 if let Some(end) = ansi_escape_sequence_end(value, start) {
823 return Some((end, 0));
824 }
825
826 let grapheme = value[start..].graphemes(true).next()?;
827 Some((start + grapheme.len(), UnicodeWidthStr::width(grapheme)))
828}
829
830pub(crate) fn display_cell_char_span(chars: &[char], cursor: usize) -> (usize, usize) {
831 if cursor >= chars.len() {
832 return (cursor, cursor);
833 }
834
835 let mut start = cursor;
836 while start > 0 && UnicodeWidthChar::width(chars[start]).unwrap_or(0) == 0 {
837 start -= 1;
838 }
839
840 let mut end = start + 1;
841 while end < chars.len() && UnicodeWidthChar::width(chars[end]).unwrap_or(0) == 0 {
842 end += 1;
843 }
844 (start, end)
845}
846
847pub(crate) fn previous_display_cell_char_span(chars: &[char], cursor: usize) -> (usize, usize) {
848 if cursor == 0 || chars.is_empty() {
849 return (0, 0);
850 }
851
852 let start = cursor.saturating_sub(1).min(chars.len() - 1);
853 display_cell_char_span(chars, start)
854}
855
856pub(crate) fn ansi_escape_sequence_end(value: &str, start: usize) -> Option<usize> {
857 let bytes = value.as_bytes();
858 if bytes.get(start) != Some(&0x1b) {
859 return None;
860 }
861 match bytes.get(start + 1).copied() {
862 Some(b'[') => bytes[start + 2..]
863 .iter()
864 .position(|byte| (0x40..=0x7e).contains(byte))
865 .map(|offset| start + 3 + offset),
866 Some(b']') => {
867 let mut index = start + 2;
868 while index < bytes.len() {
869 if bytes[index] == 0x07 {
870 return Some(index + 1);
871 }
872 if bytes[index] == 0x1b && bytes.get(index + 1) == Some(&b'\\') {
873 return Some(index + 2);
874 }
875 index += 1;
876 }
877 None
878 }
879 Some(_) => Some((start + 2).min(bytes.len())),
880 None => None,
881 }
882}
883
884pub(crate) fn osc8_link_target(sequence: &str) -> Option<&str> {
885 let body = sequence.strip_prefix("\x1b]8;")?;
886 let body = body
887 .strip_suffix("\x1b\\")
888 .or_else(|| body.strip_suffix('\x07'))?;
889 let (_, target) = body.split_once(';')?;
890 Some(target)
891}
892
893pub fn slice_visible_cols(s: &str, from: usize, to: usize) -> String {
899 if from >= to {
900 return String::new();
901 }
902
903 let plain = strip_ansi(s);
904 let mut col = 0usize;
905 let mut out = String::new();
906 let mut index = 0usize;
907
908 while let Some((end, width)) = next_display_cell_boundary(&plain, index) {
909 let cell = &plain[index..end];
910 index = end;
911 if width == 0 {
912 if col >= to {
913 break;
914 }
915 if col >= from {
916 out.push_str(cell);
917 }
918 continue;
919 }
920
921 if col >= to {
922 break;
923 }
924
925 if col >= from {
926 out.push_str(cell);
927 }
928 col = col.saturating_add(width);
929 }
930
931 out
932}
933
934pub fn wrap_words(text: &str, width: usize) -> Vec<String> {
939 wrap_words_inner(text, width, false)
940}
941
942pub(crate) fn split_lines_preserving_trailing_blank(text: &str) -> Vec<&str> {
944 text.split('\n').collect()
945}
946
947pub(crate) fn split_nonempty_lines_preserving_trailing_blank(text: &str) -> Vec<&str> {
949 if text.is_empty() {
950 Vec::new()
951 } else {
952 split_lines_preserving_trailing_blank(text)
953 }
954}
955
956pub fn wrap_words_compact(text: &str, width: usize) -> Vec<String> {
958 wrap_words_inner(text, width, true)
959}
960
961fn wrap_words_inner(text: &str, width: usize, compact: bool) -> Vec<String> {
962 if width == 0 {
963 return vec![text.to_string()];
964 }
965
966 let mut out = Vec::new();
967 for para in split_lines_preserving_trailing_blank(text) {
968 if para.trim().is_empty() {
969 if !compact {
970 out.push(String::new());
971 }
972 continue;
973 }
974
975 let mut line = String::new();
976 for word in para.split_whitespace() {
977 if line.is_empty() {
978 line.push_str(word);
979 } else if visible_len(&line) + 1 + visible_len(word) <= width {
980 line.push(' ');
981 line.push_str(word);
982 } else {
983 out.push(std::mem::take(&mut line));
984 line.push_str(word);
985 }
986
987 while visible_len(&line) > width {
988 let (head, rest) = split_visible_prefix(&line, width);
989 if head.is_empty() {
990 break;
991 }
992 out.push(head);
993 line = rest;
994 }
995 }
996
997 if !line.is_empty() {
998 out.push(line);
999 }
1000 }
1001
1002 if out.is_empty() {
1003 out.push(String::new());
1004 }
1005 out
1006}
1007
1008fn split_visible_prefix(value: &str, width: usize) -> (String, String) {
1009 let mut head = String::new();
1010 let mut used = 0usize;
1011 let mut consumed_bytes = 0usize;
1012 let mut index = 0usize;
1013
1014 while let Some((end, ch_width)) = next_display_cell_boundary(value, index) {
1015 let cell = &value[index..end];
1016 if ch_width == 0 {
1017 head.push_str(cell);
1018 consumed_bytes = end;
1019 index = end;
1020 continue;
1021 }
1022
1023 if ch_width > width {
1024 if head.is_empty() {
1025 head = truncate_visible(cell, width);
1026 consumed_bytes = end;
1027 }
1028 break;
1029 }
1030 if used > 0 && used + ch_width > width {
1031 break;
1032 }
1033
1034 head.push_str(cell);
1035 consumed_bytes = end;
1036 index = end;
1037 used += ch_width;
1038 if used >= width {
1039 break;
1040 }
1041 }
1042
1043 (head, value[consumed_bytes..].to_string())
1044}
1045
1046pub fn strip_ansi(s: &str) -> String {
1048 let mut out = String::with_capacity(s.len());
1049 let mut index = 0usize;
1050 while index < s.len() {
1051 if let Some(end) = ansi_escape_sequence_end(s, index) {
1052 index = end;
1053 continue;
1054 }
1055 let ch = s[index..].chars().next().unwrap_or_default();
1056 out.push(ch);
1057 index += ch.len_utf8();
1058 }
1059 out
1060}
1061
1062use std::str;
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067
1068 #[test]
1069 fn color_from_hex_valid() {
1070 assert_eq!(Color::from_hex("#ff0000"), Some(Color::Rgb(255, 0, 0)));
1071 assert_eq!(Color::from_hex("00ff00"), Some(Color::Rgb(0, 255, 0)));
1072 assert_eq!(Color::from_hex("#1e1e2e"), Some(Color::Rgb(30, 30, 46)));
1073 }
1074
1075 #[test]
1076 fn color_from_hex_invalid() {
1077 assert_eq!(Color::from_hex(""), None);
1078 assert_eq!(Color::from_hex("#fff"), None);
1079 assert_eq!(Color::from_hex("zzzzzz"), None);
1080 }
1081
1082 #[test]
1083 fn color_lighten() {
1084 let c = Color::Rgb(100, 100, 100);
1085 let lighter = c.lighten(0.5);
1086 match lighter {
1087 Color::Rgb(r, g, b) => {
1088 assert!(r > 100);
1089 assert!(g > 100);
1090 assert!(b > 100);
1091 }
1092 _ => panic!("expected Rgb"),
1093 }
1094 }
1095
1096 #[test]
1097 fn color_darken() {
1098 let c = Color::Rgb(200, 200, 200);
1099 let darker = c.darken(0.5);
1100 match darker {
1101 Color::Rgb(r, g, b) => {
1102 assert!(r < 200);
1103 assert!(g < 200);
1104 assert!(b < 200);
1105 }
1106 _ => panic!("expected Rgb"),
1107 }
1108 }
1109
1110 #[test]
1111 fn color_lighten_non_rgb_is_noop() {
1112 let c = Color::Red;
1113 assert_eq!(c.lighten(0.5), Color::Red);
1114 }
1115
1116 #[test]
1117 fn color_lighten_and_darken_ignore_non_finite_amounts() {
1118 let c = Color::Rgb(120, 80, 40);
1119
1120 assert_eq!(c.lighten(f64::NAN), c);
1121 assert_eq!(c.lighten(f64::INFINITY), c);
1122 assert_eq!(c.lighten(f64::NEG_INFINITY), c);
1123 assert_eq!(c.darken(f64::NAN), c);
1124 assert_eq!(c.darken(f64::INFINITY), c);
1125 assert_eq!(c.darken(f64::NEG_INFINITY), c);
1126 }
1127
1128 #[test]
1129 fn visible_len_plain() {
1130 assert_eq!(visible_len("hello"), 5);
1131 assert_eq!(visible_len(""), 0);
1132 }
1133
1134 #[test]
1135 fn visible_len_with_ansi() {
1136 assert_eq!(visible_len("\x1b[31mhello\x1b[0m"), 5);
1137 assert_eq!(visible_len("\x1b[1;32mtest\x1b[0m"), 4);
1138 assert_eq!(
1139 visible_len("\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\"),
1140 4
1141 );
1142 }
1143
1144 #[test]
1145 fn strip_ansi_removes_codes() {
1146 assert_eq!(strip_ansi("\x1b[31mred\x1b[0m"), "red");
1147 assert_eq!(
1148 strip_ansi("\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\"),
1149 "link"
1150 );
1151 assert_eq!(strip_ansi("plain"), "plain");
1152 }
1153
1154 #[test]
1155 fn color_gray() {
1156 assert_eq!(Color::gray(128), Color::Rgb(128, 128, 128));
1157 assert_eq!(Color::gray(0), Color::Rgb(0, 0, 0));
1158 assert_eq!(Color::gray(255), Color::Rgb(255, 255, 255));
1159 }
1160
1161 #[test]
1162 fn color_rgb_constructor() {
1163 assert_eq!(Color::rgb(10, 20, 30), Color::Rgb(10, 20, 30));
1164 }
1165
1166 #[test]
1167 fn color_ansi_constructor() {
1168 assert_eq!(Color::ansi(196), Color::Ansi256(196));
1169 }
1170
1171 #[test]
1172 fn pads_visible_width_with_ansi() {
1173 let styled = Style::new().fg(Color::Cyan).render("ok");
1174 let padded = pad_visible(&styled, 6);
1175
1176 assert_eq!(visible_len(&padded), 6);
1177 assert!(padded.starts_with("\x1b[36m"));
1178 }
1179
1180 #[test]
1181 fn repeats_visible_char_to_display_width() {
1182 assert_eq!(repeat_visible_char('·', 4), "····");
1183 assert_eq!(repeat_visible_char('界', 3), "界 ");
1184 assert_eq!(repeat_visible_char('\u{301}', 3), " ");
1185 assert_eq!(visible_len(&repeat_visible_char('界', 5)), 5);
1186 }
1187
1188 #[test]
1189 fn repeats_visible_pattern_to_display_width() {
1190 assert_eq!(repeat_visible("ab", 5), "ababa");
1191 assert_eq!(repeat_visible("界", 5), "界界 ");
1192 assert_eq!(repeat_visible("\u{301}", 3), " ");
1193 assert_eq!(repeat_visible("\x1b[31m-\x1b[0m", 3), "---");
1194 assert_eq!(visible_len(&repeat_visible("界a", 6)), 6);
1195 }
1196
1197 #[test]
1198 fn fits_visible_width_by_truncating_then_padding() {
1199 let out = fit_visible("一二三四五", 6);
1200
1201 assert!(visible_len(&out) <= 6, "{out:?}");
1202 assert!(out.ends_with('…') || out.ends_with(' '));
1203 }
1204
1205 #[test]
1206 fn truncate_visible_preserves_ansi_when_truncating() {
1207 let styled = Style::new().fg(Color::Red).render("abcdef");
1208 let out = truncate_visible(&styled, 4);
1209
1210 assert_eq!(visible_len(&out), 4);
1211 assert_eq!(strip_ansi(&out), "abc…");
1212 assert!(out.starts_with("\x1b[31m"));
1213 assert!(out.ends_with("\x1b[0m"));
1214 }
1215
1216 #[test]
1217 fn truncate_visible_closes_osc8_hyperlinks() {
1218 let linked = "\x1b]8;;https://example.com\x1b\\abcdef\x1b]8;;\x1b\\";
1219 let out = truncate_visible(linked, 4);
1220
1221 assert_eq!(visible_len(&out), 4);
1222 assert_eq!(strip_ansi(&out), "abc…");
1223 assert!(out.ends_with("\x1b]8;;\x1b\\"), "{out:?}");
1224 }
1225
1226 #[test]
1227 fn truncate_visible_skips_ansi_reset_between_segments() {
1228 let styled_prefix = Style::new().fg(Color::Green).render("ok");
1229 let out = truncate_visible(&format!("{styled_prefix}abcdef"), 5);
1230
1231 assert_eq!(visible_len(&out), 5);
1232 assert_eq!(strip_ansi(&out), "okab…");
1233 assert!(out.contains("\x1b[0mab"));
1234 }
1235
1236 #[test]
1237 fn truncate_visible_keeps_zero_width_marks_with_base_glyph() {
1238 let out = truncate_visible("e\u{301}xyz", 2);
1239
1240 assert_eq!(out, "e\u{301}…");
1241 assert_eq!(visible_len(&out), 2);
1242 }
1243
1244 #[test]
1245 fn truncate_visible_keeps_emoji_presentation_sequences_within_width() {
1246 let out = truncate_visible("✏️abc", 3);
1247
1248 assert_eq!(out, "✏️…");
1249 assert_eq!(visible_len(&out), 3);
1250 }
1251
1252 #[test]
1253 fn truncate_visible_does_not_split_emoji_zwj_graphemes() {
1254 let out = truncate_visible("👩💻abc", 3);
1255
1256 assert_eq!(out, "👩💻…");
1257 assert_eq!(visible_len(&out), 3);
1258 }
1259
1260 #[test]
1261 fn render_border_with_padding() {
1262 let out = Style::new()
1263 .border(Border::Rounded)
1264 .padding(1, 1)
1265 .render("x");
1266
1267 assert_eq!(
1268 out.lines().collect::<Vec<_>>(),
1269 vec!["╭───╮", "│ │", "│ x │", "│ │", "╰───╯"]
1270 );
1271 }
1272
1273 #[test]
1274 fn render_border_preserves_trailing_blank_content_row() {
1275 let out = Style::new().border(Border::Rounded).render("x\n");
1276
1277 assert_eq!(
1278 out.lines().collect::<Vec<_>>(),
1279 vec!["╭─╮", "│x│", "│ │", "╰─╯"]
1280 );
1281 }
1282
1283 #[test]
1284 fn render_bordered_content_uses_border_style_for_vertical_edges() {
1285 let out = Style::new()
1286 .fg(Color::Red)
1287 .border(Border::Rounded)
1288 .border_fg(Color::Cyan)
1289 .render("x");
1290 let lines = out.lines().collect::<Vec<_>>();
1291
1292 assert_eq!(lines[0], "\x1b[36m╭─╮\x1b[0m");
1293 assert_eq!(lines[1], "\x1b[36m│\x1b[0m\x1b[31mx\x1b[0m\x1b[36m│\x1b[0m");
1294 assert_eq!(lines[2], "\x1b[36m╰─╯\x1b[0m");
1295 assert_eq!(strip_ansi(lines[1]), "│x│");
1296 assert_eq!(visible_len(lines[1]), 3);
1297 }
1298
1299 #[test]
1300 fn render_bordered_cells_inherit_background_style() {
1301 let out = Style::new()
1302 .fg(Color::Red)
1303 .bg(Color::Blue)
1304 .border(Border::Rounded)
1305 .border_fg(Color::Cyan)
1306 .render("x");
1307 let lines = out.lines().collect::<Vec<_>>();
1308
1309 assert_eq!(lines[0], "\x1b[36;44m╭─╮\x1b[0m");
1310 assert_eq!(
1311 lines[1],
1312 "\x1b[36;44m│\x1b[0m\x1b[31;44mx\x1b[0m\x1b[36;44m│\x1b[0m"
1313 );
1314 assert_eq!(lines[2], "\x1b[36;44m╰─╯\x1b[0m");
1315 assert!(lines.iter().all(|line| visible_len(line) == 3));
1316 }
1317
1318 #[test]
1319 fn render_bordered_padding_uses_text_style_inside_vertical_edges() {
1320 let out = Style::new()
1321 .bg(Color::Blue)
1322 .border(Border::Rounded)
1323 .border_fg(Color::Cyan)
1324 .padding(1, 1)
1325 .render("x");
1326 let lines = out.lines().collect::<Vec<_>>();
1327
1328 assert_eq!(lines[0], "\x1b[36;44m╭───╮\x1b[0m");
1329 assert_eq!(
1330 lines[1],
1331 "\x1b[36;44m│\x1b[0m\x1b[44m \x1b[0m\x1b[36;44m│\x1b[0m"
1332 );
1333 assert_eq!(strip_ansi(lines[1]), "│ │");
1334 assert_eq!(visible_len(lines[1]), 5);
1335 }
1336
1337 #[test]
1338 fn render_unbordered_padding_uses_text_style() {
1339 let out = Style::new()
1340 .bg(Color::Blue)
1341 .border_fg(Color::Cyan)
1342 .padding(1, 0)
1343 .render("x");
1344 let lines = out.lines().collect::<Vec<_>>();
1345
1346 assert_eq!(
1347 lines,
1348 vec!["\x1b[44m \x1b[0m", "\x1b[44mx\x1b[0m", "\x1b[44m \x1b[0m"]
1349 );
1350 assert!(lines.iter().all(|line| visible_len(line) == 1));
1351 }
1352
1353 #[test]
1354 fn render_empty_styled_text_stays_empty() {
1355 let out = Style::new().fg(Color::Red).bold().render("");
1356
1357 assert_eq!(out, "");
1358 }
1359
1360 #[test]
1361 fn centers_visible_width_with_ansi_and_cjk() {
1362 let styled = Style::new().fg(Color::Green).render("中");
1363 let centered = center_visible(&styled, 6);
1364
1365 assert_eq!(visible_len(¢ered), 6);
1366 assert!(strip_ansi(¢ered).starts_with(" 中"));
1367 }
1368
1369 #[test]
1370 fn right_aligns_visible_width_with_ansi_and_cjk() {
1371 let styled = Style::new().fg(Color::Green).render("中");
1372 let right = right_visible(&styled, 6);
1373
1374 assert_eq!(visible_len(&right), 6);
1375 assert!(strip_ansi(&right).starts_with(" 中"));
1376 }
1377
1378 #[test]
1379 fn render_aligns_text_by_visible_width() {
1380 let styled = Style::new().fg(Color::Red).render("中");
1381 let right = Style::new().width(6).align(Align::Right).render(&styled);
1382 let center = Style::new().width(7).align(Align::Center).render("中");
1383
1384 assert_eq!(visible_len(&right), 6);
1385 assert_eq!(strip_ansi(&right), " 中");
1386 assert!(right.ends_with("\x1b[0m"));
1387 assert_eq!(visible_len(¢er), 7);
1388 assert_eq!(center, " 中 ");
1389 }
1390
1391 #[test]
1392 fn render_width_truncates_long_content_by_visible_width() {
1393 let out = Style::new().width(4).render("abcdef");
1394 let cjk = Style::new().width(5).render("中文测试");
1395
1396 assert_eq!(visible_len(&out), 4);
1397 assert_eq!(out, "abc…");
1398 assert_eq!(visible_len(&cjk), 5);
1399 assert_eq!(cjk, "中文…");
1400 }
1401
1402 #[test]
1403 fn render_bordered_width_truncates_inner_content() {
1404 let out = Style::new()
1405 .width(6)
1406 .border(Border::Rounded)
1407 .render("abcdef");
1408 let lines = out.lines().collect::<Vec<_>>();
1409
1410 assert_eq!(lines, vec!["╭────╮", "│abc…│", "╰────╯"]);
1411 assert!(lines.iter().all(|line| visible_len(line) == 6));
1412 }
1413
1414 #[test]
1415 fn render_width_includes_right_margin() {
1416 let out = Style::new()
1417 .width(8)
1418 .margin_left(1)
1419 .margin_right(2)
1420 .render("abcd");
1421
1422 assert_eq!(visible_len(&out), 8);
1423 assert_eq!(out, " abcd ");
1424 }
1425
1426 #[test]
1427 fn render_bordered_width_includes_right_margin() {
1428 let out = Style::new()
1429 .width(8)
1430 .margin_left(1)
1431 .margin_right(1)
1432 .border(Border::Rounded)
1433 .render("abcdef");
1434 let lines = out.lines().collect::<Vec<_>>();
1435
1436 assert_eq!(lines, vec![" ╭────╮ ", " │abc…│ ", " ╰────╯ "]);
1437 assert!(lines.iter().all(|line| visible_len(line) == 8));
1438 }
1439
1440 #[test]
1441 fn render_height_truncates_and_pads_content_rows() {
1442 let truncated = Style::new().width(4).height(2).render("one\ntwo\nthree");
1443 let padded = Style::new().width(4).height(3).render("one");
1444
1445 assert_eq!(truncated.lines().collect::<Vec<_>>(), vec!["one ", "two "]);
1446 assert_eq!(
1447 padded.lines().collect::<Vec<_>>(),
1448 vec!["one ", " ", " "]
1449 );
1450 }
1451
1452 #[test]
1453 fn render_bordered_height_limits_inner_content() {
1454 let out = Style::new()
1455 .width(6)
1456 .height(3)
1457 .border(Border::Rounded)
1458 .render("one\ntwo");
1459 let lines = out.lines().collect::<Vec<_>>();
1460
1461 assert_eq!(lines, vec!["╭────╮", "│one │", "╰────╯"]);
1462 assert_eq!(lines.len(), 3);
1463 }
1464
1465 #[test]
1466 fn render_height_includes_vertical_spacing() {
1467 let out = Style::new()
1468 .width(6)
1469 .height(6)
1470 .margin_top(1)
1471 .margin_bottom(1)
1472 .padding(1, 1)
1473 .render("x\ny");
1474 let lines = out.split('\n').collect::<Vec<_>>();
1475
1476 assert_eq!(lines, vec!["", " ", " x ", " y ", " ", ""]);
1477 assert_eq!(lines.len(), 6);
1478 }
1479
1480 #[test]
1481 fn slices_visible_columns_for_ascii_cjk_and_ansi() {
1482 assert_eq!(slice_visible_cols("hello", 1, 4), "ell");
1483 assert_eq!(slice_visible_cols("hello", 0, 100), "hello");
1484 assert_eq!(slice_visible_cols("你好", 0, 2), "你");
1485 assert_eq!(slice_visible_cols("你好", 2, 4), "好");
1486
1487 let styled = Style::new().fg(Color::Red).render("hello");
1488 assert_eq!(slice_visible_cols(&styled, 1, 4), "ell");
1489 }
1490
1491 #[test]
1492 fn slices_visible_columns_drop_glyph_straddling_start() {
1493 assert_eq!(slice_visible_cols("你好", 1, 4), "好");
1494 assert_eq!(slice_visible_cols("你好", 0, 3), "你好");
1495 assert_eq!(slice_visible_cols("hello", 3, 3), "");
1496 }
1497
1498 #[test]
1499 fn slices_visible_columns_keep_zero_width_marks_with_base_glyph() {
1500 assert_eq!(slice_visible_cols("e\u{301}x", 0, 1), "e\u{301}");
1501 assert_eq!(slice_visible_cols("e\u{301}x", 1, 2), "x");
1502 }
1503
1504 #[test]
1505 fn display_cell_char_span_keeps_zero_width_marks_with_base_glyph() {
1506 let chars = "e\u{301}x".chars().collect::<Vec<_>>();
1507
1508 assert_eq!(display_cell_char_span(&chars, 0), (0, 2));
1509 assert_eq!(display_cell_char_span(&chars, 1), (0, 2));
1510 assert_eq!(display_cell_char_span(&chars, 2), (2, 3));
1511 assert_eq!(display_cell_char_span(&chars, 3), (3, 3));
1512 }
1513
1514 #[test]
1515 fn previous_display_cell_char_span_keeps_zero_width_marks_with_base_glyph() {
1516 let chars = "e\u{301}x".chars().collect::<Vec<_>>();
1517
1518 assert_eq!(previous_display_cell_char_span(&chars, 0), (0, 0));
1519 assert_eq!(previous_display_cell_char_span(&chars, 2), (0, 2));
1520 assert_eq!(previous_display_cell_char_span(&chars, 3), (2, 3));
1521 }
1522
1523 #[test]
1524 fn wraps_words_on_display_columns() {
1525 let lines = wrap_words("the quick brown fox jumps", 9);
1526
1527 assert!(lines.iter().all(|line| visible_len(line) <= 9));
1528 assert_eq!(
1529 lines.join(" ").split_whitespace().collect::<Vec<_>>(),
1530 vec!["the", "quick", "brown", "fox", "jumps"]
1531 );
1532 }
1533
1534 #[test]
1535 fn wrap_words_preserves_blank_lines() {
1536 let lines = wrap_words("alpha\n\nbeta", 40);
1537
1538 assert_eq!(lines, vec!["alpha", "", "beta"]);
1539 }
1540
1541 #[test]
1542 fn wrap_words_preserves_trailing_blank_lines() {
1543 let lines = wrap_words("alpha\n", 40);
1544
1545 assert_eq!(lines, vec!["alpha", ""]);
1546 }
1547
1548 #[test]
1549 fn split_lines_preserves_trailing_blank_line() {
1550 assert_eq!(
1551 split_lines_preserving_trailing_blank("alpha\n"),
1552 vec!["alpha", ""]
1553 );
1554 assert_eq!(split_lines_preserving_trailing_blank(""), vec![""]);
1555 }
1556
1557 #[test]
1558 fn split_nonempty_lines_preserves_trailing_blank_line() {
1559 assert_eq!(
1560 split_nonempty_lines_preserving_trailing_blank(""),
1561 Vec::<&str>::new()
1562 );
1563 assert_eq!(
1564 split_nonempty_lines_preserving_trailing_blank("alpha\n"),
1565 vec!["alpha", ""]
1566 );
1567 }
1568
1569 #[test]
1570 fn compact_wrap_words_drops_blank_lines() {
1571 let lines = wrap_words_compact("alpha\n\nbeta", 40);
1572
1573 assert_eq!(lines, vec!["alpha", "beta"]);
1574 }
1575
1576 #[test]
1577 fn wrap_words_hard_breaks_wide_tokens_by_columns() {
1578 let lines = wrap_words_compact("中文测试内容", 8);
1579
1580 assert!(lines.iter().all(|line| visible_len(line) <= 8));
1581 assert_eq!(lines.concat(), "中文测试内容");
1582 }
1583
1584 #[test]
1585 fn wrap_words_hard_break_keeps_zero_width_marks_with_base_glyph() {
1586 let lines = wrap_words_compact("e\u{301}e\u{301}e", 1);
1587
1588 assert!(lines.iter().all(|line| visible_len(line) <= 1));
1589 assert_eq!(lines, vec!["e\u{301}", "e\u{301}", "e"]);
1590 }
1591
1592 #[test]
1593 fn wrap_words_hard_break_packs_zero_width_marks_by_display_width() {
1594 let lines = wrap_words_compact("e\u{301}e\u{301}e", 2);
1595
1596 assert!(lines.iter().all(|line| visible_len(line) <= 2));
1597 assert_eq!(lines, vec!["e\u{301}e\u{301}", "e"]);
1598 }
1599
1600 #[test]
1601 fn wrap_words_clips_wide_glyphs_at_single_column_width() {
1602 let lines = wrap_words_compact("中文", 1);
1603
1604 assert!(lines.iter().all(|line| visible_len(line) <= 1));
1605 }
1606}