use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
fn apply(style: Style, params: &[i32]) -> Style {
let mut s = style;
let mut i = 0;
while i < params.len() {
let p = params[i];
match p {
0 => s = Style::default(),
1 => s = s.add_modifier(Modifier::BOLD),
2 => s = s.add_modifier(Modifier::DIM),
3 => s = s.add_modifier(Modifier::ITALIC),
4 => s = s.add_modifier(Modifier::UNDERLINED),
7 => s = s.add_modifier(Modifier::REVERSED),
9 => s = s.add_modifier(Modifier::CROSSED_OUT),
21 | 22 => s = s.remove_modifier(Modifier::BOLD | Modifier::DIM),
23 => s = s.remove_modifier(Modifier::ITALIC),
24 => s = s.remove_modifier(Modifier::UNDERLINED),
27 => s = s.remove_modifier(Modifier::REVERSED),
29 => s = s.remove_modifier(Modifier::CROSSED_OUT),
30..=37 => s = s.fg(basic(p - 30)),
39 => s = s.fg(Color::Reset),
40..=47 => s = s.bg(basic(p - 40)),
49 => s = s.bg(Color::Reset),
90..=97 => s = s.fg(bright(p - 90)),
100..=107 => s = s.bg(bright(p - 100)),
38 | 48 => {
let fg = p == 38;
match params.get(i + 1) {
Some(5) => {
if let Some(&n) = params.get(i + 2) {
let c = Color::Indexed(n.clamp(0, 255) as u8);
s = if fg { s.fg(c) } else { s.bg(c) };
}
i += 2;
}
Some(2) => {
if let (Some(&r), Some(&g), Some(&b)) =
(params.get(i + 2), params.get(i + 3), params.get(i + 4))
{
let c = Color::Rgb(r as u8, g as u8, b as u8);
s = if fg { s.fg(c) } else { s.bg(c) };
}
i += 4;
}
_ => break,
}
}
_ => {}
}
i += 1;
}
s
}
fn basic(n: i32) -> Color {
match n {
0 => Color::Black,
1 => Color::Red,
2 => Color::Green,
3 => Color::Yellow,
4 => Color::Blue,
5 => Color::Magenta,
6 => Color::Cyan,
_ => Color::Gray,
}
}
fn bright(n: i32) -> Color {
match n {
0 => Color::DarkGray,
1 => Color::LightRed,
2 => Color::LightGreen,
3 => Color::LightYellow,
4 => Color::LightBlue,
5 => Color::LightMagenta,
6 => Color::LightCyan,
_ => Color::White,
}
}
pub fn to_lines(text: &str) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let mut style = Style::default();
for raw in text.lines() {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut buf = String::new();
let mut it = raw.char_indices().peekable();
while let Some((_, c)) = it.next() {
if c != '\x1b' {
buf.push(c);
continue;
}
if !buf.is_empty() {
spans.push(Span::styled(std::mem::take(&mut buf), style));
}
match it.peek().map(|&(_, c)| c) {
Some('[') => {
it.next();
let mut params: Vec<i32> = Vec::new();
let mut num = String::new();
let mut private = false;
let mut kind = None;
for (_, c) in it.by_ref() {
match c {
'0'..='9' => num.push(c),
';' | ':' => {
params.push(num.parse().unwrap_or(0));
num.clear();
}
'?' | '<' | '=' | '>' => private = true,
' '..='/' => {} _ => {
kind = Some(c);
break;
}
}
}
if !num.is_empty() || params.is_empty() {
params.push(num.parse().unwrap_or(0));
}
if kind == Some('m') && !private {
style = apply(style, ¶ms);
}
}
Some(']') => {
for (_, c) in it.by_ref() {
if c == '\x07' || c == '\x1b' {
break;
}
}
}
_ => {
it.next();
}
}
}
if !buf.is_empty() {
spans.push(Span::styled(buf, style));
}
lines.push(Line::from(spans));
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
fn texts(l: &Line) -> Vec<String> {
l.spans.iter().map(|s| s.content.to_string()).collect()
}
#[test]
fn plain_text_is_one_span() {
let l = to_lines("hello");
assert_eq!(texts(&l[0]), vec!["hello"]);
assert_eq!(l[0].spans[0].style, Style::default());
}
#[test]
fn a_colour_opens_a_span_and_a_reset_closes_it() {
let l = to_lines("a\x1b[31mred\x1b[0mb");
assert_eq!(texts(&l[0]), vec!["a", "red", "b"]);
assert_eq!(l[0].spans[1].style.fg, Some(Color::Red));
assert_eq!(l[0].spans[2].style, Style::default());
}
#[test]
fn a_reset_inside_a_sequence_clears_what_preceded_it() {
let l = to_lines("\x1b[1;33mx\x1b[0;36my");
assert!(l[0].spans[0].style.add_modifier.contains(Modifier::BOLD));
assert_eq!(l[0].spans[1].style.fg, Some(Color::Cyan));
assert!(!l[0].spans[1].style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn indexed_and_true_colour_both_land() {
let l = to_lines("\x1b[38;5;208mo\x1b[38;2;18;52;86mt");
assert_eq!(l[0].spans[0].style.fg, Some(Color::Indexed(208)));
assert_eq!(l[0].spans[1].style.fg, Some(Color::Rgb(18, 52, 86)));
}
#[test]
fn the_arguments_of_an_extended_colour_are_consumed() {
let l = to_lines("\x1b[38;5;1;1mx");
assert_eq!(l[0].spans[0].style.fg, Some(Color::Indexed(1)));
assert!(l[0].spans[0].style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn style_carries_from_one_line_to_the_next() {
let l = to_lines("\x1b[32mgreen\nstill green\x1b[0m\nplain");
assert_eq!(l[1].spans[0].style.fg, Some(Color::Green));
assert_eq!(l[2].spans[0].style, Style::default());
}
#[test]
fn a_non_sgr_sequence_is_swallowed_whole() {
let l = to_lines("a\x1b[2Jb\x1b[?25lc");
assert_eq!(texts(&l[0]).concat(), "abc");
}
#[test]
fn an_osc_title_does_not_land_on_screen() {
let l = to_lines("a\x1b]0;a window title\x07b");
assert_eq!(texts(&l[0]).concat(), "ab");
}
#[test]
fn a_bare_escape_at_the_end_does_not_panic() {
assert_eq!(to_lines("a\x1b").len(), 1);
assert_eq!(to_lines("\x1b[").len(), 1);
assert_eq!(to_lines("\x1b[38;5").len(), 1);
}
#[test]
fn blank_lines_survive() {
assert_eq!(to_lines("a\n\nb").len(), 3);
}
}