use std::fmt::Write as _;
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier};
#[must_use]
pub fn render_text(buffer: &Buffer) -> String {
let area = buffer.area;
(0..area.height)
.map(|row| {
(0..area.width)
.map(|col| buffer[(area.x + col, area.y + row)].symbol())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn render_ansi(buffer: &Buffer) -> String {
let area = buffer.area;
let mut out = String::new();
for row in 0..area.height {
let mut current = String::new();
for col in 0..area.width {
let cell = &buffer[(area.x + col, area.y + row)];
let wanted = sgr(cell.fg, cell.bg, cell.modifier);
if wanted != current {
out.push_str("\u{1b}[0m");
out.push_str(&wanted);
current = wanted;
}
out.push_str(cell.symbol());
}
out.push_str("\u{1b}[0m");
out.push('\n');
}
out
}
fn sgr(fg: Color, bg: Color, modifier: Modifier) -> String {
let mut out = String::new();
if modifier.contains(Modifier::REVERSED) {
out.push_str("\u{1b}[7m");
}
match fg {
Color::Reset => {}
Color::Indexed(index) => {
let _ = write!(out, "\u{1b}[38;5;{index}m");
}
Color::Red => out.push_str("\u{1b}[31m"),
Color::Green => out.push_str("\u{1b}[32m"),
Color::Yellow => out.push_str("\u{1b}[33m"),
Color::DarkGray => out.push_str("\u{1b}[90m"),
_ => {}
}
match bg {
Color::Reset => {}
Color::Indexed(index) => {
let _ = write!(out, "\u{1b}[48;5;{index}m");
}
Color::Red => out.push_str("\u{1b}[41m"),
Color::Green => out.push_str("\u{1b}[42m"),
Color::Yellow => out.push_str("\u{1b}[43m"),
Color::DarkGray => out.push_str("\u{1b}[100m"),
_ => {}
}
out
}
#[cfg(test)]
mod tests {
use ratatui::layout::Rect;
use ratatui::style::Style;
use super::super::scene;
use super::super::scene::Scene;
use super::*;
#[test]
fn the_ansi_dump_emits_a_bands_reverse_video_and_a_grounds_background() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
buffer[(0, 0)].set_style(Style::default().add_modifier(Modifier::REVERSED));
buffer[(1, 0)].set_style(Style::default().bg(Color::Indexed(235)));
let dump = render_ansi(&buffer);
assert!(dump.contains("\u{1b}[7m"), "reverse video");
assert!(dump.contains("\u{1b}[48;5;235m"), "an indexed background");
}
#[test]
fn the_plain_renderer_is_one_line_per_row_and_no_escapes() {
let text = render_text(&scene(Scene::HealthyWide).1);
assert_eq!(text.lines().count(), 30);
assert!(!text.contains('\u{1b}'), "plain means plain");
for line in text.lines() {
assert_eq!(line.chars().count(), 120, "every row is the full width");
}
}
#[test]
fn the_ansi_renderer_colours_the_errored_row_and_always_resets() {
let ansi = render_ansi(&scene(Scene::Errored).1);
assert!(
ansi.contains("\u{1b}[38;5;166m"),
"bark, on the errored status"
);
for line in ansi.lines() {
assert!(
line.is_empty() || line.ends_with("\u{1b}[0m"),
"every line resets before its newline"
);
}
}
}