Skip to main content

binocular/preview/request/git/
ansi.rs

1use ratatui::style::{Color, Modifier, Style};
2use ratatui::text::{Line, Span, Text};
3
4pub(crate) fn parse_ansi_text(text: String) -> Text<'static> {
5    let mut lines = Vec::new();
6    let mut spans = Vec::new();
7    let mut current = String::new();
8    let mut style = Style::default();
9    let bytes = text.as_bytes();
10    let mut idx = 0usize;
11
12    while idx < bytes.len() {
13        if bytes[idx] == 0x1b && idx + 1 < bytes.len() && bytes[idx + 1] == b'[' {
14            if !current.is_empty() {
15                spans.push(Span::styled(std::mem::take(&mut current), style));
16            }
17            idx += 2;
18            let start = idx;
19            while idx < bytes.len() && bytes[idx] != b'm' {
20                idx += 1;
21            }
22            if idx >= bytes.len() {
23                break;
24            }
25            let codes = &text[start..idx];
26            style = apply_ansi_codes(style, codes);
27            idx += 1;
28            continue;
29        }
30
31        let ch = text[idx..].chars().next().unwrap();
32        idx += ch.len_utf8();
33        if ch == '\n' {
34            if !current.is_empty() {
35                spans.push(Span::styled(std::mem::take(&mut current), style));
36            }
37            lines.push(Line::from(std::mem::take(&mut spans)));
38        } else if ch != '\r' {
39            current.push(ch);
40        }
41    }
42
43    if !current.is_empty() || !spans.is_empty() {
44        spans.push(Span::styled(current, style));
45        lines.push(Line::from(spans));
46    }
47
48    Text::from(lines)
49}
50
51fn apply_ansi_codes(mut style: Style, codes: &str) -> Style {
52    if codes.is_empty() {
53        return Style::default();
54    }
55
56    for code in codes.split(';').filter(|part| !part.is_empty()) {
57        match code {
58            "0" => style = Style::default(),
59            "1" => style = style.add_modifier(Modifier::BOLD),
60            "2" => style = style.add_modifier(Modifier::DIM),
61            "22" => style = style.remove_modifier(Modifier::BOLD | Modifier::DIM),
62            "30" => style = style.fg(Color::Black),
63            "31" => style = style.fg(Color::Red),
64            "32" => style = style.fg(Color::Green),
65            "33" => style = style.fg(Color::Yellow),
66            "34" => style = style.fg(Color::Blue),
67            "35" => style = style.fg(Color::Magenta),
68            "36" => style = style.fg(Color::Cyan),
69            "37" => style = style.fg(Color::Gray),
70            "39" => style = style.fg(Color::Reset),
71            "90" => style = style.fg(Color::DarkGray),
72            "91" => style = style.fg(Color::LightRed),
73            "92" => style = style.fg(Color::LightGreen),
74            "93" => style = style.fg(Color::LightYellow),
75            "94" => style = style.fg(Color::LightBlue),
76            "95" => style = style.fg(Color::LightMagenta),
77            "96" => style = style.fg(Color::LightCyan),
78            "97" => style = style.fg(Color::White),
79            _ => {}
80        }
81    }
82    style
83}