1use regex::Regex;
8use std::collections::HashMap;
9use std::str::FromStr;
10use std::sync::OnceLock;
11
12static ANSI_REGEX: OnceLock<std::result::Result<Regex, regex::Error>> = 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() -> Option<&'static Regex> {
33 ANSI_REGEX.get_or_init(|| Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")).as_ref().ok()
34}
35
36pub mod control {
38 pub const BEL: &str = "\x07";
40 pub const BS: &str = "\x08";
42 pub const HT: &str = "\x09";
44 pub const LF: &str = "\x0A";
46 pub const VT: &str = "\x0B";
48 pub const FF: &str = "\x0C";
50 pub const CR: &str = "\x0D";
52 pub const ESC: &str = "\x1B";
54 pub const DEL: &str = "\x7F";
56}
57
58pub mod csi {
60 pub const CSI: &str = "\x1B[";
62
63 pub fn cursor_up(n: usize) -> String {
65 format!("\x1B[{n}A")
66 }
67
68 pub fn cursor_down(n: usize) -> String {
70 format!("\x1B[{n}B")
71 }
72
73 pub fn cursor_forward(n: usize) -> String {
75 format!("\x1B[{n}C")
76 }
77
78 pub fn cursor_back(n: usize) -> String {
80 format!("\x1B[{n}D")
81 }
82
83 pub fn cursor_next_line(n: usize) -> String {
85 format!("\x1B[{n}E")
86 }
87
88 pub fn cursor_prev_line(n: usize) -> String {
90 format!("\x1B[{n}F")
91 }
92
93 pub fn cursor_horizontal(n: usize) -> String {
95 format!("\x1B[{n}G")
96 }
97
98 pub fn cursor_position(row: usize, col: usize) -> String {
100 format!("\x1B[{row};{col}H")
101 }
102
103 pub fn erase_in_display(n: usize) -> String {
105 format!("\x1B[{n}J")
106 }
107
108 pub fn erase_in_line(n: usize) -> String {
110 format!("\x1B[{n}K")
111 }
112
113 pub fn scroll_up(n: usize) -> String {
115 format!("\x1B[{n}S")
116 }
117
118 pub fn scroll_down(n: usize) -> String {
120 format!("\x1B[{n}T")
121 }
122
123 pub const REQUEST_CURSOR_POSITION: &str = "\x1B[6n";
125
126 pub const SAVE_CURSOR_POSITION: &str = "\x1B[s";
128
129 pub const RESTORE_CURSOR_POSITION: &str = "\x1B[u";
131
132 pub const HIDE_CURSOR: &str = "\x1B[?25l";
134
135 pub const SHOW_CURSOR: &str = "\x1B[?25h";
137
138 pub const ENABLE_ALT_SCREEN: &str = "\x1B[?1049h";
140
141 pub const DISABLE_ALT_SCREEN: &str = "\x1B[?1049l";
143}
144
145pub mod sgr {
147 pub const RESET: &str = "\x1B[0m";
149
150 pub const BOLD: &str = "\x1B[1m";
152
153 pub const DIM: &str = "\x1B[2m";
155
156 pub const ITALIC: &str = "\x1B[3m";
158
159 pub const UNDERLINE: &str = "\x1B[4m";
161
162 pub const BLINK: &str = "\x1B[5m";
164
165 pub const RAPID_BLINK: &str = "\x1B[6m";
167
168 pub const REVERSE: &str = "\x1B[7m";
170
171 pub const CONCEAL: &str = "\x1B[8m";
173
174 pub const STRIKE: &str = "\x1B[9m";
176
177 pub const PRIMARY_FONT: &str = "\x1B[10m";
179
180 pub fn alt_font(n: usize) -> String {
182 if !(1..=9).contains(&n) {
183 return String::new();
184 }
185 format!("\x1B[{}m", 10 + n)
186 }
187
188 pub const FRAKTUR: &str = "\x1B[20m";
190
191 pub const DOUBLE_UNDERLINE: &str = "\x1B[21m";
193
194 pub const NORMAL_INTENSITY: &str = "\x1B[22m";
196
197 pub const NO_ITALIC: &str = "\x1B[23m";
199
200 pub const NO_UNDERLINE: &str = "\x1B[24m";
202
203 pub const NO_BLINK: &str = "\x1B[25m";
205
206 pub const PROPORTIONAL_SPACING: &str = "\x1B[26m";
208
209 pub const NO_REVERSE: &str = "\x1B[27m";
211
212 pub const REVEAL: &str = "\x1B[28m";
214
215 pub const NO_STRIKE: &str = "\x1B[29m";
217
218 pub fn fg_color(n: usize) -> String {
220 format!("\x1B[{n}m")
221 }
222
223 pub fn bg_color(n: usize) -> String {
225 format!("\x1B[{n}m")
226 }
227
228 pub fn fg_color_256(n: u8) -> String {
230 format!("\x1B[38;5;{n}m")
231 }
232
233 pub fn bg_color_256(n: u8) -> String {
235 format!("\x1B[48;5;{n}m")
236 }
237
238 pub fn fg_color_rgb(r: u8, g: u8, b: u8) -> String {
240 format!("\x1B[38;2;{r};{g};{b}m")
241 }
242
243 pub fn bg_color_rgb(r: u8, g: u8, b: u8) -> String {
245 format!("\x1B[48;2;{r};{g};{b}m")
246 }
247
248 pub const DEFAULT_FG: &str = "\x1B[39m";
250
251 pub const DEFAULT_BG: &str = "\x1B[49m";
253
254 pub const NO_PROPORTIONAL_SPACING: &str = "\x1B[50m";
256
257 pub const FRAMED: &str = "\x1B[51m";
259
260 pub const ENCIRCLED: &str = "\x1B[52m";
262
263 pub const OVERLINED: &str = "\x1B[53m";
265
266 pub const NO_FRAMED: &str = "\x1B[54m";
268
269 pub const NO_OVERLINED: &str = "\x1B[55m";
271
272 pub const IDEOGRAM_UNDERLINE: &str = "\x1B[60m";
274
275 pub const IDEOGRAM_DOUBLE_UNDERLINE: &str = "\x1B[61m";
277
278 pub const IDEOGRAM_OVERLINE: &str = "\x1B[62m";
280
281 pub const IDEOGRAM_DOUBLE_OVERLINE: &str = "\x1B[63m";
283
284 pub const IDEOGRAM_STRESS: &str = "\x1B[64m";
286
287 pub const NO_IDEOGRAM: &str = "\x1B[65m";
289
290 pub const SUPERSCRIPT: &str = "\x1B[73m";
292
293 pub const SUBSCRIPT: &str = "\x1B[74m";
295
296 pub const NO_SCRIPT: &str = "\x1B[75m";
298}
299
300pub mod osc {
302 pub fn set_title(title: &str) -> String {
304 format!("\x1B]0;{title}\x07")
305 }
306
307 pub fn set_window_icon_title(title: &str) -> String {
309 format!("\x1B]2;{title}\x07")
310 }
311
312 pub fn set_icon_title(title: &str) -> String {
314 format!("\x1B]1;{title}\x07")
315 }
316
317 pub fn set_color(num: u8, rgb: &str) -> String {
319 format!("\x1B]4;{num};{rgb}\x07")
320 }
321
322 pub fn hyperlink(url: &str, text: &str) -> String {
324 format!("\x1B]8;;{url}\x07{text}\x1B]8;;\x07")
325 }
326}
327
328pub mod mouse {
330 pub const NORMAL_TRACKING: &str = "\x1B[?1000h";
332
333 pub const NO_NORMAL_TRACKING: &str = "\x1B[?1000l";
335
336 pub const HIGHLIGHT_TRACKING: &str = "\x1B[?1001h";
338
339 pub const NO_HIGHLIGHT_TRACKING: &str = "\x1B[?1001l";
341
342 pub const BUTTON_EVENT_TRACKING: &str = "\x1B[?1002h";
344
345 pub const NO_BUTTON_EVENT_TRACKING: &str = "\x1B[?1002l";
347
348 pub const ANY_EVENT_TRACKING: &str = "\x1B[?1003h";
350
351 pub const NO_ANY_EVENT_TRACKING: &str = "\x1B[?1003l";
353
354 pub const FOCUS_TRACKING: &str = "\x1B[?1004h";
356
357 pub const NO_FOCUS_TRACKING: &str = "\x1B[?1004l";
359
360 pub const EXTENDED_COORDINATES: &str = "\x1B[?1006h";
362
363 pub const NO_EXTENDED_COORDINATES: &str = "\x1B[?1006l";
365
366 pub const SGR_COORDINATES: &str = "\x1B[?1016h";
368
369 pub const NO_SGR_COORDINATES: &str = "\x1B[?1016l";
371}
372
373pub mod modes {
375 pub const APPLICATION_CURSOR_KEYS: &str = "\x1B[?1h";
377
378 pub const NORMAL_CURSOR_KEYS: &str = "\x1B[?1l";
380
381 pub const ANSI_MODE: &str = "\x1B[?2h";
383
384 pub const VT52_MODE: &str = "\x1B[?2l";
386
387 pub const MODE_132_COLUMN: &str = "\x1B[?3h";
389
390 pub const MODE_80_COLUMN: &str = "\x1B[?3l";
392
393 pub const SMOOTH_SCROLL: &str = "\x1B[?4h";
395
396 pub const JUMP_SCROLL: &str = "\x1B[?4l";
398
399 pub const REVERSE_SCREEN: &str = "\x1B[?5h";
401
402 pub const NORMAL_SCREEN: &str = "\x1B[?5l";
404
405 pub const APPLICATION_KEYPAD: &str = "\x1B[?66h";
407
408 pub const NUMERIC_KEYPAD: &str = "\x1B[?66l";
410
411 pub const WRAPAROUND: &str = "\x1B[?7h";
413
414 pub const NO_WRAPAROUND: &str = "\x1B[?7l";
416
417 pub const AUTOREPEAT_KEYS: &str = "\x1B[?8h";
419
420 pub const NO_AUTOREPEAT_KEYS: &str = "\x1B[?8l";
422
423 pub const MOUSE_TRACKING: &str = "\x1B[?9h";
425
426 pub const NO_MOUSE_TRACKING: &str = "\x1B[?9l";
428
429 pub const SHOW_TOOLBAR: &str = "\x1B[?10h";
431
432 pub const HIDE_TOOLBAR: &str = "\x1B[?10l";
434
435 pub const BLINKING_CURSOR: &str = "\x1B[?12h";
437
438 pub const NO_BLINKING_CURSOR: &str = "\x1B[?12l";
440
441 pub const PRINT_FORM_FEED: &str = "\x1B[?18h";
443
444 pub const NO_PRINT_FORM_FEED: &str = "\x1B[?18l";
446
447 pub const PRINT_SCREEN: &str = "\x1B[?19h";
449
450 pub const NO_PRINT_SCREEN: &str = "\x1B[?19l";
452
453 pub const NEWLINE_MODE: &str = "\x1B[20h";
455
456 pub const NO_NEWLINE_MODE: &str = "\x1B[20l";
458}
459
460#[derive(Debug, Clone, PartialEq, Eq)]
462pub enum TermColor {
463 Basic(u8),
465
466 Color256(u8),
468
469 TrueColor {
471 r: u8,
473 g: u8,
475 b: u8,
477 },
478}
479
480impl TermColor {
481 pub fn fg_code(&self) -> String {
483 match self {
484 TermColor::Basic(n) if *n < 8 => format!("\x1B[{}m", 30 + n),
485 TermColor::Basic(n) if *n < 16 => format!("\x1B[{}m", 82 + n),
486 TermColor::Basic(n) | TermColor::Color256(n) => sgr::fg_color_256(*n),
487 TermColor::TrueColor { r, g, b } => sgr::fg_color_rgb(*r, *g, *b),
488 }
489 }
490
491 pub fn bg_code(&self) -> String {
493 match self {
494 TermColor::Basic(n) if *n < 8 => format!("\x1B[{}m", 40 + n),
495 TermColor::Basic(n) if *n < 16 => format!("\x1B[{}m", 92 + n),
496 TermColor::Basic(n) | TermColor::Color256(n) => sgr::bg_color_256(*n),
497 TermColor::TrueColor { r, g, b } => sgr::bg_color_rgb(*r, *g, *b),
498 }
499 }
500}
501
502pub fn parse_ansi_sequences(text: &str) -> Vec<(usize, String)> {
514 ansi_regex().map_or_else(Vec::new, |regex| {
515 regex.find_iter(text).map(|m| (m.start(), m.as_str().to_string())).collect()
516 })
517}
518
519pub fn color_name_to_code(name: &str) -> Option<TermColor> {
521 if let Some((_, code)) = BASIC_COLORS.iter().find(|(color, _)| color.eq_ignore_ascii_case(name))
523 {
524 return Some(TermColor::Basic(*code));
525 }
526
527 if let Ok(num) = u8::from_str(name) {
529 return Some(TermColor::Color256(num));
530 }
531
532 if name.starts_with('#') || (name.len() == 6 && name.chars().all(|c| c.is_ascii_hexdigit())) {
534 if let Some(color) = parse_hex_color(name) {
535 return Some(color);
536 }
537 }
538
539 None
540}
541
542fn parse_hex_color(hex: &str) -> Option<TermColor> {
546 let hex = hex.trim_start_matches('#');
547
548 match hex.len() {
549 3 => {
550 let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
552 let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
553 let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
554
555 let r = r * 17;
557 let g = g * 17;
558 let b = b * 17;
559
560 Some(TermColor::TrueColor { r, g, b })
561 }
562 6 => {
563 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
565 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
566 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
567
568 Some(TermColor::TrueColor { r, g, b })
569 }
570 _ => None,
571 }
572}
573
574pub fn format_ansi_text(
591 text: &str,
592 bold: bool,
593 italic: bool,
594 underline: bool,
595 fg_color: Option<&TermColor>,
596 bg_color: Option<&TermColor>,
597) -> String {
598 let mut result = String::new();
599
600 if bold {
602 result.push_str(sgr::BOLD);
603 }
604 if italic {
605 result.push_str(sgr::ITALIC);
606 }
607 if underline {
608 result.push_str(sgr::UNDERLINE);
609 }
610
611 if let Some(color) = fg_color {
613 result.push_str(&color.fg_code());
614 }
615 if let Some(color) = bg_color {
616 result.push_str(&color.bg_code());
617 }
618
619 result.push_str(text);
621 result.push_str(sgr::RESET);
622
623 result
624}
625
626pub fn strip_ansi_codes(text: &str) -> String {
636 ansi_regex().map_or_else(|| text.to_string(), |regex| regex.replace_all(text, "").to_string())
637}
638
639pub fn extract_ansi_styles(text: &str) -> Vec<(usize, HashMap<String, String>)> {
649 let mut result = Vec::new();
650 let mut current_styles = HashMap::new();
651 let mut pos = 0;
652
653 for (idx, seq) in parse_ansi_sequences(text) {
654 if idx > pos {
656 result.push((pos, current_styles.clone()));
657 }
659
660 if seq == sgr::RESET {
662 current_styles.clear();
663 } else if seq == sgr::BOLD {
664 current_styles.insert("weight".to_string(), "bold".to_string());
665 } else if seq == sgr::ITALIC {
666 current_styles.insert("style".to_string(), "italic".to_string());
667 } else if seq == sgr::UNDERLINE {
668 current_styles.insert("text-decoration".to_string(), "underline".to_string());
669 } else if seq.starts_with("\x1B[38;") {
670 current_styles.insert("color".to_string(), extract_color_from_seq(&seq));
671 } else if seq.starts_with("\x1B[48;") {
672 current_styles.insert("background-color".to_string(), extract_color_from_seq(&seq));
673 }
674
675 pos = idx + seq.len();
677 }
678
679 if pos < text.len() {
681 result.push((pos, current_styles));
682 }
683
684 result
685}
686
687fn extract_color_from_seq(seq: &str) -> String {
689 if seq.starts_with("\x1B[38;5;") || seq.starts_with("\x1B[48;5;") {
690 let parts: Vec<&str> = seq.split(';').collect();
692 if parts.len() >= 3 {
693 if let Some(color_part) = parts[2].strip_suffix('m') {
694 return format!("color-{color_part}");
695 }
696 }
697 } else if seq.starts_with("\x1B[38;2;") || seq.starts_with("\x1B[48;2;") {
698 let parts: Vec<&str> = seq.split(';').collect();
700 if parts.len() >= 5 {
701 let r = parts[2];
702 let g = parts[3];
703 let b =
704 if let Some(stripped) = parts[4].strip_suffix('m') { stripped } else { parts[4] };
705 return format!("rgb({r},{g},{b})");
706 }
707 }
708
709 "unknown".to_string()
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 #[test]
717 fn test_term_color() {
718 let red = TermColor::Basic(1);
719 assert_eq!(red.fg_code(), "\x1B[31m");
720 assert_eq!(red.bg_code(), "\x1B[41m");
721
722 let color256 = TermColor::Color256(128);
723 assert_eq!(color256.fg_code(), "\x1B[38;5;128m");
724 assert_eq!(color256.bg_code(), "\x1B[48;5;128m");
725
726 let true_color = TermColor::TrueColor { r: 255, g: 128, b: 64 };
727 assert_eq!(true_color.fg_code(), "\x1B[38;2;255;128;64m");
728 assert_eq!(true_color.bg_code(), "\x1B[48;2;255;128;64m");
729 }
730
731 #[test]
732 fn test_color_name_to_code() {
733 assert_eq!(color_name_to_code("red"), Some(TermColor::Basic(1)));
734 assert_eq!(color_name_to_code("brightblue"), Some(TermColor::Basic(12)));
735 assert_eq!(color_name_to_code("123"), Some(TermColor::Color256(123)));
736
737 assert_eq!(
738 color_name_to_code("#ff00ff"),
739 Some(TermColor::TrueColor { r: 255, g: 0, b: 255 })
740 );
741
742 assert_eq!(color_name_to_code("#f0f"), Some(TermColor::TrueColor { r: 255, g: 0, b: 255 }));
743 }
744
745 #[test]
746 fn test_format_ansi_text() {
747 let text = format_ansi_text("Hello", true, false, true, Some(&TermColor::Basic(1)), None);
748 assert_eq!(text, "\x1B[1m\x1B[4m\x1B[31mHello\x1B[0m");
749 }
750
751 #[test]
752 fn test_strip_ansi_codes() {
753 let input = "\x1B[1m\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m";
754 let output = strip_ansi_codes(input);
755 assert_eq!(output, "Hello World");
756 }
757
758 #[test]
759 fn test_parse_ansi_sequences() {
760 let input = "Normal \x1B[1mBold\x1B[0m Normal";
761 let sequences = parse_ansi_sequences(input);
762 assert_eq!(sequences.len(), 2);
763 assert_eq!(sequences[0], (7, "\x1B[1m".to_string()));
764 assert_eq!(sequences[1], (15, "\x1B[0m".to_string()));
765 }
766}