diff --git a/crates/ciadm/src/main.rs b/crates/ciadm/src/main.rs index 4798f6e..94ecd8d 100644 --- a/crates/ciadm/src/main.rs +++ b/crates/ciadm/src/main.rs @@ -714,7 +714,7 @@ impl TuiApp { let text = if self.logs_text.is_empty() { Text::from("No logs available.") } else { - Text::from(self.logs_text.as_str()) + ansi_to_text(&self.logs_text) }; let paragraph = Paragraph::new(text) .block(Block::default().borders(Borders::ALL).title(title)) @@ -737,3 +737,170 @@ impl TuiApp { 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> = Vec::new(); + let mut lines: Vec> = Vec::new(); + + let mut flush_buf = |buf: &mut String, line: &mut Vec>, 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, ¶ms); + } + 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 { + if bytes.is_empty() { + return Vec::new(); + } + let s = String::from_utf8_lossy(bytes); + s.split(';') + .filter_map(|p| p.parse::().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, + } +}