use std::fmt::Write as _;
use super::{Cell, Color, Screen, Style};
const DEFAULT_FG: &str = "#d4d4d4";
const DEFAULT_BG: &str = "#1e1e1e";
fn palette(index: u8) -> String {
const ANSI: [&str; 16] = [
"#000000", "#cd3131", "#0dbc79", "#e5e510", "#2472c8", "#bc3fbc", "#11a8cd", "#e5e5e5",
"#666666", "#f14c4c", "#23d18b", "#f5f543", "#3b8eea", "#d670d6", "#29b8db", "#ffffff",
];
match index {
0..=15 => ANSI[usize::from(index)].to_owned(),
16..=231 => {
let i = index - 16;
let level = |v: u8| if v == 0 { 0 } else { 55 + 40 * u32::from(v) };
format!(
"#{:02x}{:02x}{:02x}",
level(i / 36),
level((i / 6) % 6),
level(i % 6)
)
}
232..=255 => {
let grey = 8 + 10 * u32::from(index - 232);
format!("#{grey:02x}{grey:02x}{grey:02x}")
}
}
}
fn css_color(color: Color, default: &str) -> String {
match color {
Color::Default => default.to_owned(),
Color::Indexed(i) => palette(i),
Color::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
}
}
fn painted(style: &Style) -> (String, String) {
let fg = css_color(style.fg, DEFAULT_FG);
let bg = css_color(style.bg, DEFAULT_BG);
if style.reverse {
(bg, fg)
} else {
(fg, bg)
}
}
fn sgr(style: &Style) -> String {
let mut params = vec!["0".to_owned()];
for (on, code) in [
(style.bold, "1"),
(style.dim, "2"),
(style.italic, "3"),
(style.underline, "4"),
(style.blink, "5"),
(style.reverse, "7"),
(style.conceal, "8"),
(style.strikethrough, "9"),
] {
if on {
params.push(code.to_owned());
}
}
match style.fg {
Color::Default => {}
Color::Indexed(i @ 0..=7) => params.push((30 + u16::from(i)).to_string()),
Color::Indexed(i @ 8..=15) => params.push((82 + u16::from(i)).to_string()),
Color::Indexed(i) => params.push(format!("38;5;{i}")),
Color::Rgb(r, g, b) => params.push(format!("38;2;{r};{g};{b}")),
}
match style.bg {
Color::Default => {}
Color::Indexed(i @ 0..=7) => params.push((40 + u16::from(i)).to_string()),
Color::Indexed(i @ 8..=15) => params.push((92 + u16::from(i)).to_string()),
Color::Indexed(i) => params.push(format!("48;5;{i}")),
Color::Rgb(r, g, b) => params.push(format!("48;2;{r};{g};{b}")),
}
format!("\x1b[{}m", params.join(";"))
}
fn glyph(cell: &Cell) -> &str {
if cell.contents().is_empty() {
" "
} else {
cell.contents()
}
}
fn escape_xml(text: &str, out: &mut String) {
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
other => out.push(other),
}
}
}
fn runs(screen: &Screen, row: u16) -> Vec<(u16, u16, Style, String)> {
let mut runs: Vec<(u16, u16, Style, String)> = Vec::new();
for col in 0..screen.cols() {
let Some(cell) = screen.cell(row, col) else {
break;
};
if cell.is_wide_continuation() {
if let Some(last) = runs.last_mut() {
last.1 += 1;
}
continue;
}
let style = *cell.style();
match runs.last_mut() {
Some(last) if last.2 == style => {
last.1 += 1;
last.3.push_str(glyph(cell));
}
_ => runs.push((col, 1, style, glyph(cell).to_owned())),
}
}
runs
}
impl Screen {
#[must_use]
pub fn to_ansi(&self) -> String {
let mut out = String::new();
for row in 0..self.rows() {
for (_, _, style, text) in runs(self, row) {
if !style.is_default() {
out.push_str(&sgr(&style));
}
out.push_str(&text);
if !style.is_default() {
out.push_str("\x1b[0m");
}
}
out.push('\n');
}
out
}
#[must_use]
pub fn to_svg(&self) -> String {
const CELL_W: u32 = 9;
const CELL_H: u32 = 18;
let width = u32::from(self.cols()) * CELL_W;
let height = u32::from(self.rows()) * CELL_H;
let mut out = String::new();
let _ = writeln!(
out,
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" \
viewBox=\"0 0 {width} {height}\" font-family=\"'JetBrains Mono', 'Fira Code', \
Menlo, Consolas, 'DejaVu Sans Mono', monospace\" font-size=\"14\">"
);
let _ = writeln!(
out,
"<rect width=\"{width}\" height=\"{height}\" fill=\"{DEFAULT_BG}\"/>"
);
for row in 0..self.rows() {
let y = u32::from(row) * CELL_H;
for (col, cols, style, text) in runs(self, row) {
let (fg, bg) = painted(&style);
let x = u32::from(col) * CELL_W;
if bg != DEFAULT_BG {
let _ = writeln!(
out,
"<rect x=\"{x}\" y=\"{y}\" width=\"{}\" height=\"{CELL_H}\" fill=\"{bg}\"/>",
u32::from(cols) * CELL_W
);
}
let shown = if style.conceal { "" } else { text.trim_end() };
if shown.is_empty() {
continue;
}
let _ = write!(
out,
"<text x=\"{x}\" y=\"{}\" fill=\"{fg}\" xml:space=\"preserve\"",
y + 14
);
if style.bold {
out.push_str(" font-weight=\"bold\"");
}
if style.italic {
out.push_str(" font-style=\"italic\"");
}
if style.dim {
out.push_str(" opacity=\"0.6\"");
}
let mut decoration = Vec::new();
if style.underline {
decoration.push("underline");
}
if style.strikethrough {
decoration.push("line-through");
}
if !decoration.is_empty() {
let _ = write!(out, " text-decoration=\"{}\"", decoration.join(" "));
}
out.push('>');
escape_xml(shown, &mut out);
out.push_str("</text>\n");
}
}
out.push_str("</svg>\n");
out
}
#[must_use]
pub fn to_html(&self) -> String {
let mut out = format!(
"<pre style=\"background:{DEFAULT_BG};color:{DEFAULT_FG};font-family:monospace;\
line-height:1.2;padding:8px\">"
);
for row in 0..self.rows() {
for (_, cols, style, text) in runs(self, row) {
let shown: String = if style.conceal {
" ".repeat(usize::from(cols))
} else {
text
};
if style.is_default() {
escape_xml(&shown, &mut out);
continue;
}
let (fg, bg) = painted(&style);
let mut css = format!("color:{fg};background:{bg}");
if style.bold {
css.push_str(";font-weight:bold");
}
if style.italic {
css.push_str(";font-style:italic");
}
if style.dim {
css.push_str(";opacity:0.6");
}
let mut decoration = Vec::new();
if style.underline {
decoration.push("underline");
}
if style.strikethrough {
decoration.push("line-through");
}
if !decoration.is_empty() {
let _ = write!(css, ";text-decoration:{}", decoration.join(" "));
}
let _ = write!(out, "<span style=\"{css}\">");
escape_xml(&shown, &mut out);
out.push_str("</span>");
}
out.push('\n');
}
out.push_str("</pre>\n");
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_palette_is_xterms() {
assert_eq!(palette(1), "#cd3131");
assert_eq!(palette(16), "#000000");
assert_eq!(palette(21), "#0000ff");
assert_eq!(palette(196), "#ff0000");
assert_eq!(palette(232), "#080808");
assert_eq!(palette(255), "#eeeeee");
}
#[test]
fn sgr_sets_the_style_from_a_reset() {
let mut style = Style {
bold: true,
fg: Color::Indexed(1),
bg: Color::Rgb(30, 30, 46),
..Style::default()
};
assert_eq!(sgr(&style), "\x1b[0;1;31;48;2;30;30;46m");
style.fg = Color::Indexed(9);
assert_eq!(sgr(&style), "\x1b[0;1;91;48;2;30;30;46m");
style.fg = Color::Indexed(200);
assert_eq!(sgr(&style), "\x1b[0;1;38;5;200;48;2;30;30;46m");
}
}