use crate::render::Modifier;
use crate::style::Color;
#[derive(Clone, Debug, PartialEq)]
pub struct AnsiSpan {
pub text: String,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub modifiers: Modifier,
}
impl Default for AnsiSpan {
fn default() -> Self {
Self {
text: String::new(),
fg: None,
bg: None,
modifiers: Modifier::empty(),
}
}
}
impl AnsiSpan {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
pub fn is_default(&self) -> bool {
self.fg.is_none() && self.bg.is_none() && self.modifiers.is_empty()
}
}
#[derive(Clone, Debug, Default)]
struct AnsiState {
fg: Option<Color>,
bg: Option<Color>,
modifiers: Modifier,
}
impl AnsiState {
fn reset(&mut self) {
self.fg = None;
self.bg = None;
self.modifiers = Modifier::empty();
}
fn to_span(&self, text: String) -> AnsiSpan {
AnsiSpan {
text,
fg: self.fg,
bg: self.bg,
modifiers: self.modifiers,
}
}
}
pub fn parse_ansi(input: &str) -> Vec<AnsiSpan> {
let mut spans = Vec::new();
let mut state = AnsiState::default();
let mut current_text = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
let mut params = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() || c == ';' {
params.push(c);
chars.next();
} else {
break;
}
}
if let Some(&final_char) = chars.peek() {
chars.next();
if final_char == 'm' {
if !current_text.is_empty() {
spans.push(state.to_span(current_text.clone()));
current_text.clear();
}
apply_sgr(&mut state, ¶ms);
}
}
} else if chars.peek() == Some(&']') {
chars.next(); while let Some(&c) = chars.peek() {
chars.next();
if c == '\x07' || (c == '\x1b' && chars.peek() == Some(&'\\')) {
if c == '\x1b' {
chars.next(); }
break;
}
}
}
} else {
current_text.push(ch);
}
}
if !current_text.is_empty() {
spans.push(state.to_span(current_text));
}
spans
}
fn apply_sgr(state: &mut AnsiState, params: &str) {
if params.is_empty() {
state.reset();
return;
}
let codes: Vec<u8> = params.split(';').filter_map(|s| s.parse().ok()).collect();
let mut i = 0;
while i < codes.len() {
match codes[i] {
0 => state.reset(),
1 => state.modifiers |= Modifier::BOLD,
2 => state.modifiers |= Modifier::DIM,
3 => state.modifiers |= Modifier::ITALIC,
4 => state.modifiers |= Modifier::UNDERLINE,
5 | 6 => {} 7 => {} 8 => {} 9 => state.modifiers |= Modifier::CROSSED_OUT,
22 => state.modifiers.remove(Modifier::BOLD | Modifier::DIM),
23 => state.modifiers.remove(Modifier::ITALIC),
24 => state.modifiers.remove(Modifier::UNDERLINE),
25 => {} 27 => {} 28 => {} 29 => state.modifiers.remove(Modifier::CROSSED_OUT),
30 => state.fg = Some(Color::BLACK),
31 => state.fg = Some(Color::RED),
32 => state.fg = Some(Color::GREEN),
33 => state.fg = Some(Color::YELLOW),
34 => state.fg = Some(Color::BLUE),
35 => state.fg = Some(Color::MAGENTA),
36 => state.fg = Some(Color::CYAN),
37 => state.fg = Some(Color::WHITE),
39 => state.fg = None,
40 => state.bg = Some(Color::BLACK),
41 => state.bg = Some(Color::RED),
42 => state.bg = Some(Color::GREEN),
43 => state.bg = Some(Color::YELLOW),
44 => state.bg = Some(Color::BLUE),
45 => state.bg = Some(Color::MAGENTA),
46 => state.bg = Some(Color::CYAN),
47 => state.bg = Some(Color::WHITE),
49 => state.bg = None,
90 => state.fg = Some(Color::rgb(128, 128, 128)), 91 => state.fg = Some(Color::rgb(255, 85, 85)), 92 => state.fg = Some(Color::rgb(85, 255, 85)), 93 => state.fg = Some(Color::rgb(255, 255, 85)), 94 => state.fg = Some(Color::rgb(85, 85, 255)), 95 => state.fg = Some(Color::rgb(255, 85, 255)), 96 => state.fg = Some(Color::rgb(85, 255, 255)), 97 => state.fg = Some(Color::rgb(255, 255, 255)),
100 => state.bg = Some(Color::rgb(128, 128, 128)),
101 => state.bg = Some(Color::rgb(255, 85, 85)),
102 => state.bg = Some(Color::rgb(85, 255, 85)),
103 => state.bg = Some(Color::rgb(255, 255, 85)),
104 => state.bg = Some(Color::rgb(85, 85, 255)),
105 => state.bg = Some(Color::rgb(255, 85, 255)),
106 => state.bg = Some(Color::rgb(85, 255, 255)),
107 => state.bg = Some(Color::rgb(255, 255, 255)),
38 if i + 2 < codes.len() && codes[i + 1] == 5 => {
state.fg = Some(color_256(codes[i + 2]));
i += 2;
}
48 if i + 2 < codes.len() && codes[i + 1] == 5 => {
state.bg = Some(color_256(codes[i + 2]));
i += 2;
}
#[allow(clippy::unnecessary_min_or_max)]
38 if i + 4 < codes.len() && codes[i + 1] == 2 => {
let r = codes[i + 2].min(255);
let g = codes[i + 3].min(255);
let b = codes[i + 4].min(255);
state.fg = Some(Color::rgb(r, g, b));
i += 4;
}
#[allow(clippy::unnecessary_min_or_max)]
48 if i + 4 < codes.len() && codes[i + 1] == 2 => {
let r = codes[i + 2].min(255);
let g = codes[i + 3].min(255);
let b = codes[i + 4].min(255);
state.bg = Some(Color::rgb(r, g, b));
i += 4;
}
_ => {}
}
i += 1;
}
}
fn color_256(code: u8) -> Color {
match code {
0 => Color::BLACK,
1 => Color::RED,
2 => Color::GREEN,
3 => Color::YELLOW,
4 => Color::BLUE,
5 => Color::MAGENTA,
6 => Color::CYAN,
7 => Color::WHITE,
8 => Color::rgb(128, 128, 128),
9 => Color::rgb(255, 85, 85),
10 => Color::rgb(85, 255, 85),
11 => Color::rgb(255, 255, 85),
12 => Color::rgb(85, 85, 255),
13 => Color::rgb(255, 85, 255),
14 => Color::rgb(85, 255, 255),
15 => Color::rgb(255, 255, 255),
16..=231 => {
let n = code - 16;
let r = (n / 36) % 6;
let g = (n / 6) % 6;
let b = n % 6;
Color::rgb(
if r > 0 { r * 40 + 55 } else { 0 },
if g > 0 { g * 40 + 55 } else { 0 },
if b > 0 { b * 40 + 55 } else { 0 },
)
}
232..=255 => {
let gray = (code - 232) * 10 + 8;
Color::rgb(gray, gray, gray)
}
}
}
pub fn strip_ansi(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
while let Some(&c) = chars.peek() {
chars.next();
if c.is_ascii_alphabetic() {
break;
}
}
} else if chars.peek() == Some(&']') {
chars.next();
while let Some(&c) = chars.peek() {
chars.next();
if c == '\x07' || (c == '\x1b' && chars.peek() == Some(&'\\')) {
if c == '\x1b' {
chars.next();
}
break;
}
}
}
} else {
result.push(ch);
}
}
result
}
pub fn ansi_len(input: &str) -> usize {
strip_ansi(input).len()
}
pub mod codes {
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const ITALIC: &str = "\x1b[3m";
pub const UNDERLINE: &str = "\x1b[4m";
pub const FG_BLACK: &str = "\x1b[30m";
pub const FG_RED: &str = "\x1b[31m";
pub const FG_GREEN: &str = "\x1b[32m";
pub const FG_YELLOW: &str = "\x1b[33m";
pub const FG_BLUE: &str = "\x1b[34m";
pub const FG_MAGENTA: &str = "\x1b[35m";
pub const FG_CYAN: &str = "\x1b[36m";
pub const FG_WHITE: &str = "\x1b[37m";
pub const BG_BLACK: &str = "\x1b[40m";
pub const BG_RED: &str = "\x1b[41m";
pub const BG_GREEN: &str = "\x1b[42m";
pub const BG_YELLOW: &str = "\x1b[43m";
pub const BG_BLUE: &str = "\x1b[44m";
pub const BG_MAGENTA: &str = "\x1b[45m";
pub const BG_CYAN: &str = "\x1b[46m";
pub const BG_WHITE: &str = "\x1b[47m";
pub fn fg_256(code: u8) -> String {
format!("\x1b[38;5;{}m", code)
}
pub fn bg_256(code: u8) -> String {
format!("\x1b[48;5;{}m", code)
}
pub fn fg_rgb(r: u8, g: u8, b: u8) -> String {
format!("\x1b[38;2;{};{};{}m", r, g, b)
}
pub fn bg_rgb(r: u8, g: u8, b: u8) -> String {
format!("\x1b[48;2;{};{};{}m", r, g, b)
}
}