1use regex::Regex;
8use std::collections::HashMap;
9use std::str::FromStr;
10use std::sync::OnceLock;
11
12static ANSI_REGEX: OnceLock<Regex> = OnceLock::new();
13const BASIC_COLORS: &[(&str, u8)] = &[
14 ("black", 0),
15 ("red", 1),
16 ("green", 2),
17 ("yellow", 3),
18 ("blue", 4),
19 ("magenta", 5),
20 ("cyan", 6),
21 ("white", 7),
22 ("brightblack", 8),
23 ("brightred", 9),
24 ("brightgreen", 10),
25 ("brightyellow", 11),
26 ("brightblue", 12),
27 ("brightmagenta", 13),
28 ("brightcyan", 14),
29 ("brightwhite", 15),
30];
31
32fn ansi_regex() -> &'static Regex {
33 #[allow(clippy::expect_used)]
38 ANSI_REGEX.get_or_init(|| {
39 Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").expect("ANSI regex must compile")
40 })
41}
42
43pub mod control {
45 pub const BEL: &str = "\x07";
47 pub const BS: &str = "\x08";
49 pub const HT: &str = "\x09";
51 pub const LF: &str = "\x0A";
53 pub const VT: &str = "\x0B";
55 pub const FF: &str = "\x0C";
57 pub const CR: &str = "\x0D";
59 pub const ESC: &str = "\x1B";
61 pub const DEL: &str = "\x7F";
63}
64
65pub mod csi {
67 pub const CSI: &str = "\x1B[";
69
70 pub fn cursor_up(n: usize) -> String {
72 format!("\x1B[{n}A")
73 }
74
75 pub fn cursor_down(n: usize) -> String {
77 format!("\x1B[{n}B")
78 }
79
80 pub fn cursor_forward(n: usize) -> String {
82 format!("\x1B[{n}C")
83 }
84
85 pub fn cursor_back(n: usize) -> String {
87 format!("\x1B[{n}D")
88 }
89
90 pub fn cursor_next_line(n: usize) -> String {
92 format!("\x1B[{n}E")
93 }
94
95 pub fn cursor_prev_line(n: usize) -> String {
97 format!("\x1B[{n}F")
98 }
99
100 pub fn cursor_horizontal(n: usize) -> String {
102 format!("\x1B[{n}G")
103 }
104
105 pub fn cursor_position(row: usize, col: usize) -> String {
107 format!("\x1B[{row};{col}H")
108 }
109
110 pub fn erase_in_display(n: usize) -> String {
112 format!("\x1B[{n}J")
113 }
114
115 pub fn erase_in_line(n: usize) -> String {
117 format!("\x1B[{n}K")
118 }
119
120 pub fn scroll_up(n: usize) -> String {
122 format!("\x1B[{n}S")
123 }
124
125 pub fn scroll_down(n: usize) -> String {
127 format!("\x1B[{n}T")
128 }
129
130 pub const REQUEST_CURSOR_POSITION: &str = "\x1B[6n";
132
133 pub const SAVE_CURSOR_POSITION: &str = "\x1B[s";
135
136 pub const RESTORE_CURSOR_POSITION: &str = "\x1B[u";
138
139 pub const HIDE_CURSOR: &str = "\x1B[?25l";
141
142 pub const SHOW_CURSOR: &str = "\x1B[?25h";
144
145 pub const ENABLE_ALT_SCREEN: &str = "\x1B[?1049h";
147
148 pub const DISABLE_ALT_SCREEN: &str = "\x1B[?1049l";
150}
151
152pub mod sgr {
154 pub const RESET: &str = "\x1B[0m";
156
157 pub const BOLD: &str = "\x1B[1m";
159
160 pub const DIM: &str = "\x1B[2m";
162
163 pub const ITALIC: &str = "\x1B[3m";
165
166 pub const UNDERLINE: &str = "\x1B[4m";
168
169 pub const BLINK: &str = "\x1B[5m";
171
172 pub const RAPID_BLINK: &str = "\x1B[6m";
174
175 pub const REVERSE: &str = "\x1B[7m";
177
178 pub const CONCEAL: &str = "\x1B[8m";
180
181 pub const STRIKE: &str = "\x1B[9m";
183
184 pub const PRIMARY_FONT: &str = "\x1B[10m";
186
187 pub fn alt_font(n: usize) -> String {
189 if !(1..=9).contains(&n) {
190 return String::new();
191 }
192 format!("\x1B[{}m", 10 + n)
193 }
194
195 pub const FRAKTUR: &str = "\x1B[20m";
197
198 pub const DOUBLE_UNDERLINE: &str = "\x1B[21m";
200
201 pub const NORMAL_INTENSITY: &str = "\x1B[22m";
203
204 pub const NO_ITALIC: &str = "\x1B[23m";
206
207 pub const NO_UNDERLINE: &str = "\x1B[24m";
209
210 pub const NO_BLINK: &str = "\x1B[25m";
212
213 pub const PROPORTIONAL_SPACING: &str = "\x1B[26m";
215
216 pub const NO_REVERSE: &str = "\x1B[27m";
218
219 pub const REVEAL: &str = "\x1B[28m";
221
222 pub const NO_STRIKE: &str = "\x1B[29m";
224
225 pub fn fg_color(n: usize) -> String {
227 format!("\x1B[{n}m")
228 }
229
230 pub fn bg_color(n: usize) -> String {
232 format!("\x1B[{n}m")
233 }
234
235 pub fn fg_color_256(n: u8) -> String {
237 format!("\x1B[38;5;{n}m")
238 }
239
240 pub fn bg_color_256(n: u8) -> String {
242 format!("\x1B[48;5;{n}m")
243 }
244
245 pub fn fg_color_rgb(r: u8, g: u8, b: u8) -> String {
247 format!("\x1B[38;2;{r};{g};{b}m")
248 }
249
250 pub fn bg_color_rgb(r: u8, g: u8, b: u8) -> String {
252 format!("\x1B[48;2;{r};{g};{b}m")
253 }
254
255 pub const DEFAULT_FG: &str = "\x1B[39m";
257
258 pub const DEFAULT_BG: &str = "\x1B[49m";
260
261 pub const NO_PROPORTIONAL_SPACING: &str = "\x1B[50m";
263
264 pub const FRAMED: &str = "\x1B[51m";
266
267 pub const ENCIRCLED: &str = "\x1B[52m";
269
270 pub const OVERLINED: &str = "\x1B[53m";
272
273 pub const NO_FRAMED: &str = "\x1B[54m";
275
276 pub const NO_OVERLINED: &str = "\x1B[55m";
278
279 pub const IDEOGRAM_UNDERLINE: &str = "\x1B[60m";
281
282 pub const IDEOGRAM_DOUBLE_UNDERLINE: &str = "\x1B[61m";
284
285 pub const IDEOGRAM_OVERLINE: &str = "\x1B[62m";
287
288 pub const IDEOGRAM_DOUBLE_OVERLINE: &str = "\x1B[63m";
290
291 pub const IDEOGRAM_STRESS: &str = "\x1B[64m";
293
294 pub const NO_IDEOGRAM: &str = "\x1B[65m";
296
297 pub const SUPERSCRIPT: &str = "\x1B[73m";
299
300 pub const SUBSCRIPT: &str = "\x1B[74m";
302
303 pub const NO_SCRIPT: &str = "\x1B[75m";
305}
306
307pub mod osc {
309 pub fn set_title(title: &str) -> String {
311 format!("\x1B]0;{title}\x07")
312 }
313
314 pub fn set_window_icon_title(title: &str) -> String {
316 format!("\x1B]2;{title}\x07")
317 }
318
319 pub fn set_icon_title(title: &str) -> String {
321 format!("\x1B]1;{title}\x07")
322 }
323
324 pub fn set_color(num: u8, rgb: &str) -> String {
326 format!("\x1B]4;{num};{rgb}\x07")
327 }
328
329 pub fn hyperlink(url: &str, text: &str) -> String {
331 format!("\x1B]8;;{url}\x07{text}\x1B]8;;\x07")
332 }
333}
334
335pub mod mouse {
337 pub const NORMAL_TRACKING: &str = "\x1B[?1000h";
339
340 pub const NO_NORMAL_TRACKING: &str = "\x1B[?1000l";
342
343 pub const HIGHLIGHT_TRACKING: &str = "\x1B[?1001h";
345
346 pub const NO_HIGHLIGHT_TRACKING: &str = "\x1B[?1001l";
348
349 pub const BUTTON_EVENT_TRACKING: &str = "\x1B[?1002h";
351
352 pub const NO_BUTTON_EVENT_TRACKING: &str = "\x1B[?1002l";
354
355 pub const ANY_EVENT_TRACKING: &str = "\x1B[?1003h";
357
358 pub const NO_ANY_EVENT_TRACKING: &str = "\x1B[?1003l";
360
361 pub const FOCUS_TRACKING: &str = "\x1B[?1004h";
363
364 pub const NO_FOCUS_TRACKING: &str = "\x1B[?1004l";
366
367 pub const EXTENDED_COORDINATES: &str = "\x1B[?1006h";
369
370 pub const NO_EXTENDED_COORDINATES: &str = "\x1B[?1006l";
372
373 pub const SGR_COORDINATES: &str = "\x1B[?1016h";
375
376 pub const NO_SGR_COORDINATES: &str = "\x1B[?1016l";
378}
379
380pub mod modes {
382 pub const APPLICATION_CURSOR_KEYS: &str = "\x1B[?1h";
384
385 pub const NORMAL_CURSOR_KEYS: &str = "\x1B[?1l";
387
388 pub const ANSI_MODE: &str = "\x1B[?2h";
390
391 pub const VT52_MODE: &str = "\x1B[?2l";
393
394 pub const MODE_132_COLUMN: &str = "\x1B[?3h";
396
397 pub const MODE_80_COLUMN: &str = "\x1B[?3l";
399
400 pub const SMOOTH_SCROLL: &str = "\x1B[?4h";
402
403 pub const JUMP_SCROLL: &str = "\x1B[?4l";
405
406 pub const REVERSE_SCREEN: &str = "\x1B[?5h";
408
409 pub const NORMAL_SCREEN: &str = "\x1B[?5l";
411
412 pub const APPLICATION_KEYPAD: &str = "\x1B[?66h";
414
415 pub const NUMERIC_KEYPAD: &str = "\x1B[?66l";
417
418 pub const WRAPAROUND: &str = "\x1B[?7h";
420
421 pub const NO_WRAPAROUND: &str = "\x1B[?7l";
423
424 pub const AUTOREPEAT_KEYS: &str = "\x1B[?8h";
426
427 pub const NO_AUTOREPEAT_KEYS: &str = "\x1B[?8l";
429
430 pub const MOUSE_TRACKING: &str = "\x1B[?9h";
432
433 pub const NO_MOUSE_TRACKING: &str = "\x1B[?9l";
435
436 pub const SHOW_TOOLBAR: &str = "\x1B[?10h";
438
439 pub const HIDE_TOOLBAR: &str = "\x1B[?10l";
441
442 pub const BLINKING_CURSOR: &str = "\x1B[?12h";
444
445 pub const NO_BLINKING_CURSOR: &str = "\x1B[?12l";
447
448 pub const PRINT_FORM_FEED: &str = "\x1B[?18h";
450
451 pub const NO_PRINT_FORM_FEED: &str = "\x1B[?18l";
453
454 pub const PRINT_SCREEN: &str = "\x1B[?19h";
456
457 pub const NO_PRINT_SCREEN: &str = "\x1B[?19l";
459
460 pub const NEWLINE_MODE: &str = "\x1B[20h";
462
463 pub const NO_NEWLINE_MODE: &str = "\x1B[20l";
465}
466
467#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum TermColor {
470 Basic(u8),
472
473 Color256(u8),
475
476 TrueColor {
478 r: u8,
480 g: u8,
482 b: u8,
484 },
485}
486
487impl TermColor {
488 pub fn fg_code(&self) -> String {
490 match self {
491 TermColor::Basic(n) if *n < 8 => format!("\x1B[{}m", 30 + n),
492 TermColor::Basic(n) if *n < 16 => format!("\x1B[{}m", 82 + n),
493 TermColor::Basic(n) | TermColor::Color256(n) => sgr::fg_color_256(*n),
494 TermColor::TrueColor { r, g, b } => sgr::fg_color_rgb(*r, *g, *b),
495 }
496 }
497
498 pub fn bg_code(&self) -> String {
500 match self {
501 TermColor::Basic(n) if *n < 8 => format!("\x1B[{}m", 40 + n),
502 TermColor::Basic(n) if *n < 16 => format!("\x1B[{}m", 92 + n),
503 TermColor::Basic(n) | TermColor::Color256(n) => sgr::bg_color_256(*n),
504 TermColor::TrueColor { r, g, b } => sgr::bg_color_rgb(*r, *g, *b),
505 }
506 }
507}
508
509pub fn parse_ansi_sequences(text: &str) -> Vec<(usize, String)> {
521 ansi_regex().find_iter(text).map(|m| (m.start(), m.as_str().to_string())).collect()
522}
523
524pub fn color_name_to_code(name: &str) -> Option<TermColor> {
526 if let Some((_, code)) = BASIC_COLORS.iter().find(|(color, _)| color.eq_ignore_ascii_case(name))
528 {
529 return Some(TermColor::Basic(*code));
530 }
531
532 if let Ok(num) = u8::from_str(name) {
534 return Some(TermColor::Color256(num));
535 }
536
537 if name.starts_with('#') || (name.len() == 6 && name.chars().all(|c| c.is_ascii_hexdigit())) {
539 if let Some(color) = parse_hex_color(name) {
540 return Some(color);
541 }
542 }
543
544 None
545}
546
547fn parse_hex_color(hex: &str) -> Option<TermColor> {
551 let hex = hex.trim_start_matches('#');
552
553 match hex.len() {
554 3 => {
555 let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
557 let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
558 let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
559
560 let r = r * 17;
562 let g = g * 17;
563 let b = b * 17;
564
565 Some(TermColor::TrueColor { r, g, b })
566 }
567 6 => {
568 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
570 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
571 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
572
573 Some(TermColor::TrueColor { r, g, b })
574 }
575 _ => None,
576 }
577}
578
579pub fn format_ansi_text(
596 text: &str,
597 bold: bool,
598 italic: bool,
599 underline: bool,
600 fg_color: Option<&TermColor>,
601 bg_color: Option<&TermColor>,
602) -> String {
603 let mut result = String::new();
604
605 if bold {
607 result.push_str(sgr::BOLD);
608 }
609 if italic {
610 result.push_str(sgr::ITALIC);
611 }
612 if underline {
613 result.push_str(sgr::UNDERLINE);
614 }
615
616 if let Some(color) = fg_color {
618 result.push_str(&color.fg_code());
619 }
620 if let Some(color) = bg_color {
621 result.push_str(&color.bg_code());
622 }
623
624 result.push_str(text);
626 result.push_str(sgr::RESET);
627
628 result
629}
630
631pub fn strip_ansi_codes(text: &str) -> String {
641 crate::state::terminal::strip_ansi_codes(text)
646}
647
648pub fn extract_ansi_styles(text: &str) -> Vec<(usize, HashMap<String, String>)> {
658 let mut result = Vec::new();
659 let mut current_styles = HashMap::new();
660 let mut pos = 0;
661
662 for (idx, seq) in parse_ansi_sequences(text) {
663 if idx > pos {
665 result.push((pos, current_styles.clone()));
666 }
668
669 if seq == sgr::RESET {
671 current_styles.clear();
672 } else if seq == sgr::BOLD {
673 current_styles.insert("weight".to_string(), "bold".to_string());
674 } else if seq == sgr::ITALIC {
675 current_styles.insert("style".to_string(), "italic".to_string());
676 } else if seq == sgr::UNDERLINE {
677 current_styles.insert("text-decoration".to_string(), "underline".to_string());
678 } else if seq.starts_with("\x1B[38;") {
679 current_styles.insert("color".to_string(), extract_color_from_seq(&seq));
680 } else if seq.starts_with("\x1B[48;") {
681 current_styles.insert("background-color".to_string(), extract_color_from_seq(&seq));
682 }
683
684 pos = idx + seq.len();
686 }
687
688 if pos < text.len() {
690 result.push((pos, current_styles));
691 }
692
693 result
694}
695
696fn extract_color_from_seq(seq: &str) -> String {
698 if seq.starts_with("\x1B[38;5;") || seq.starts_with("\x1B[48;5;") {
699 let parts: Vec<&str> = seq.split(';').collect();
701 if parts.len() >= 3 {
702 if let Some(color_part) = parts[2].strip_suffix('m') {
703 return format!("color-{color_part}");
704 }
705 }
706 } else if seq.starts_with("\x1B[38;2;") || seq.starts_with("\x1B[48;2;") {
707 let parts: Vec<&str> = seq.split(';').collect();
709 if parts.len() >= 5 {
710 let r = parts[2];
711 let g = parts[3];
712 let b =
713 if let Some(stripped) = parts[4].strip_suffix('m') { stripped } else { parts[4] };
714 return format!("rgb({r},{g},{b})");
715 }
716 }
717
718 "unknown".to_string()
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
726 fn test_term_color() {
727 let red = TermColor::Basic(1);
728 assert_eq!(red.fg_code(), "\x1B[31m");
729 assert_eq!(red.bg_code(), "\x1B[41m");
730
731 let color256 = TermColor::Color256(128);
732 assert_eq!(color256.fg_code(), "\x1B[38;5;128m");
733 assert_eq!(color256.bg_code(), "\x1B[48;5;128m");
734
735 let true_color = TermColor::TrueColor { r: 255, g: 128, b: 64 };
736 assert_eq!(true_color.fg_code(), "\x1B[38;2;255;128;64m");
737 assert_eq!(true_color.bg_code(), "\x1B[48;2;255;128;64m");
738 }
739
740 #[test]
741 fn test_color_name_to_code() {
742 assert_eq!(color_name_to_code("red"), Some(TermColor::Basic(1)));
743 assert_eq!(color_name_to_code("brightblue"), Some(TermColor::Basic(12)));
744 assert_eq!(color_name_to_code("123"), Some(TermColor::Color256(123)));
745
746 assert_eq!(
747 color_name_to_code("#ff00ff"),
748 Some(TermColor::TrueColor { r: 255, g: 0, b: 255 })
749 );
750
751 assert_eq!(color_name_to_code("#f0f"), Some(TermColor::TrueColor { r: 255, g: 0, b: 255 }));
752 }
753
754 #[test]
755 fn test_format_ansi_text() {
756 let text = format_ansi_text("Hello", true, false, true, Some(&TermColor::Basic(1)), None);
757 assert_eq!(text, "\x1B[1m\x1B[4m\x1B[31mHello\x1B[0m");
758 }
759
760 #[test]
761 fn test_strip_ansi_codes() {
762 let input = "\x1B[1m\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m";
763 let output = strip_ansi_codes(input);
764 assert_eq!(output, "Hello World");
765 }
766
767 #[test]
768 fn test_parse_ansi_sequences() {
769 let input = "Normal \x1B[1mBold\x1B[0m Normal";
770 let sequences = parse_ansi_sequences(input);
771 assert_eq!(sequences.len(), 2);
772 assert_eq!(sequences[0], (7, "\x1B[1m".to_string()));
773 assert_eq!(sequences[1], (15, "\x1B[0m".to_string()));
774 }
775}