Add ANSI escape sequence parsing for enhanced log rendering

- Replace plain text rendering with `ansi_to_text` for displaying logs with styled ANSI sequences in TUI.
- Implement parsing logic for SGR parameters to apply text styling (e.g., bold, italic, colors).
- Extend TUI functionality to support dynamic styling based on ANSI codes.

Signed-off-by: Till Wegmueller <toasterson@gmail.com>
This commit is contained in:
Till Wegmueller 2026-01-25 23:15:12 +01:00
parent e76f4c0278
commit c0a7f7e3f2
No known key found for this signature in database

View file

@ -714,7 +714,7 @@ impl TuiApp {
let text = if self.logs_text.is_empty() { let text = if self.logs_text.is_empty() {
Text::from("No logs available.") Text::from("No logs available.")
} else { } else {
Text::from(self.logs_text.as_str()) ansi_to_text(&self.logs_text)
}; };
let paragraph = Paragraph::new(text) let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).title(title)) .block(Block::default().borders(Borders::ALL).title(title))
@ -737,3 +737,170 @@ impl TuiApp {
frame.render_widget(footer, area); frame.render_widget(footer, area);
} }
} }
fn ansi_to_text(input: &str) -> Text<'static> {
let bytes = input.as_bytes();
let mut i = 0;
let mut style = Style::default();
let mut buf = String::new();
let mut line: Vec<Span<'static>> = Vec::new();
let mut lines: Vec<Line<'static>> = Vec::new();
let mut flush_buf = |buf: &mut String, line: &mut Vec<Span<'static>>, style: Style| {
if !buf.is_empty() {
let chunk = std::mem::take(buf);
line.push(Span::styled(chunk, style));
}
};
while i < bytes.len() {
if bytes[i] == 0x1b {
if i + 1 < bytes.len() && bytes[i + 1] == b'[' {
let mut j = i + 2;
while j < bytes.len() && !bytes[j].is_ascii_alphabetic() {
j += 1;
}
if j < bytes.len() {
let cmd = bytes[j];
if cmd == b'm' {
flush_buf(&mut buf, &mut line, style);
let params = parse_sgr_params(&bytes[i + 2..j]);
apply_sgr(&mut style, &params);
}
i = j + 1;
continue;
} else {
break;
}
}
}
let ch = input[i..].chars().next().unwrap_or('\0');
if ch == '\n' {
flush_buf(&mut buf, &mut line, style);
lines.push(Line::from(std::mem::take(&mut line)));
i += 1;
continue;
}
if ch != '\r' {
buf.push(ch);
}
i += ch.len_utf8();
}
flush_buf(&mut buf, &mut line, style);
lines.push(Line::from(line));
Text::from(lines)
}
fn parse_sgr_params(bytes: &[u8]) -> Vec<u16> {
if bytes.is_empty() {
return Vec::new();
}
let s = String::from_utf8_lossy(bytes);
s.split(';')
.filter_map(|p| p.parse::<u16>().ok())
.collect()
}
fn apply_sgr(style: &mut Style, params: &[u16]) {
if params.is_empty() {
*style = Style::default();
return;
}
let mut i = 0;
while i < params.len() {
match params[i] {
0 => *style = Style::default(),
1 => *style = style.add_modifier(Modifier::BOLD),
2 => *style = style.add_modifier(Modifier::DIM),
3 => *style = style.add_modifier(Modifier::ITALIC),
4 => *style = style.add_modifier(Modifier::UNDERLINED),
7 => *style = style.add_modifier(Modifier::REVERSED),
22 => *style = style.remove_modifier(Modifier::BOLD | Modifier::DIM),
23 => *style = style.remove_modifier(Modifier::ITALIC),
24 => *style = style.remove_modifier(Modifier::UNDERLINED),
27 => *style = style.remove_modifier(Modifier::REVERSED),
30..=37 => {
*style = style.fg(sgr_basic_color(params[i] - 30));
}
40..=47 => {
*style = style.bg(sgr_basic_color(params[i] - 40));
}
90..=97 => {
*style = style.fg(sgr_bright_color(params[i] - 90));
}
100..=107 => {
*style = style.bg(sgr_bright_color(params[i] - 100));
}
38 => {
if i + 1 < params.len() {
match params[i + 1] {
5 if i + 2 < params.len() => {
*style = style.fg(Color::Indexed(params[i + 2] as u8));
i += 2;
}
2 if i + 4 < params.len() => {
*style = style.fg(Color::Rgb(
params[i + 2] as u8,
params[i + 3] as u8,
params[i + 4] as u8,
));
i += 4;
}
_ => {}
}
}
}
48 => {
if i + 1 < params.len() {
match params[i + 1] {
5 if i + 2 < params.len() => {
*style = style.bg(Color::Indexed(params[i + 2] as u8));
i += 2;
}
2 if i + 4 < params.len() => {
*style = style.bg(Color::Rgb(
params[i + 2] as u8,
params[i + 3] as u8,
params[i + 4] as u8,
));
i += 4;
}
_ => {}
}
}
}
39 => *style = style.fg(Color::Reset),
49 => *style = style.bg(Color::Reset),
_ => {}
}
i += 1;
}
}
fn sgr_basic_color(idx: u16) -> Color {
match idx {
0 => Color::Black,
1 => Color::Red,
2 => Color::Green,
3 => Color::Yellow,
4 => Color::Blue,
5 => Color::Magenta,
6 => Color::Cyan,
7 => Color::White,
_ => Color::Reset,
}
}
fn sgr_bright_color(idx: u16) -> Color {
match idx {
0 => Color::DarkGray,
1 => Color::LightRed,
2 => Color::LightGreen,
3 => Color::LightYellow,
4 => Color::LightBlue,
5 => Color::LightMagenta,
6 => Color::LightCyan,
7 => Color::White,
_ => Color::Reset,
}
}