use crate::color::parse_color;
use crate::tokenizer::tokenize;
use image::Rgba;
use std::collections::HashMap;
pub const ANSI_RESET: &str = "\x1b[0m";
pub fn color_to_ansi_bg(rgba: Rgba<u8>) -> String {
if rgba[3] == 0 {
"\x1b[48;5;236m".to_string()
} else {
format!("\x1b[48;2;{};{};{}m", rgba[0], rgba[1], rgba[2])
}
}
pub fn render_ansi_grid(
grid: &[String],
palette: &HashMap<String, String>,
aliases: &HashMap<char, String>,
) -> (String, String) {
let mut output = String::new();
let mut legend_entries: Vec<(char, String, String)> = Vec::new();
let mut seen_tokens: HashMap<String, char> = HashMap::new();
let reverse_aliases: HashMap<String, char> =
aliases.iter().map(|(c, name)| (name.clone(), *c)).collect();
let mut next_auto_alias = 'a';
for row in grid {
let (tokens, _warnings) = tokenize(row);
for token in &tokens {
let display_char = if let Some(&c) = reverse_aliases.get(token) {
c
} else if let Some(&c) = seen_tokens.get(token) {
c
} else {
let c = if token == "{_}" {
'_'
} else if token.len() == 3 {
token.chars().nth(1).unwrap_or(next_auto_alias)
} else {
let c = next_auto_alias;
if next_auto_alias < 'z' {
next_auto_alias = (next_auto_alias as u8 + 1) as char;
}
c
};
seen_tokens.insert(token.clone(), c);
let hex_color = palette.get(token).cloned().unwrap_or_else(|| "???".to_string());
let name = token.trim_matches(|c| c == '{' || c == '}').to_string();
legend_entries.push((c, name, hex_color));
c
};
let hex_color = palette.get(token).cloned().unwrap_or_else(|| "#808080".to_string());
let rgba = parse_color(&hex_color).unwrap_or(Rgba([128, 128, 128, 255]));
let ansi_bg = color_to_ansi_bg(rgba);
output.push_str(&ansi_bg);
output.push(' ');
output.push(display_char);
output.push(' ');
output.push_str(ANSI_RESET);
}
output.push('\n');
}
let mut legend = String::from("\nLegend:\n");
legend_entries.sort_by_key(|(c, _, _)| *c);
let mut seen_chars: std::collections::HashSet<char> = std::collections::HashSet::new();
for (c, name, hex) in legend_entries {
if seen_chars.insert(c) {
legend.push_str(&format!(" {} = {:16} ({})\n", c, name, hex));
}
}
(output, legend)
}
pub fn render_coordinate_grid(grid: &[String], full_names: bool) -> String {
if grid.is_empty() {
return String::new();
}
let parsed_rows: Vec<Vec<String>> = grid
.iter()
.map(|row| {
let (tokens, _) = tokenize(row);
tokens
})
.collect();
let max_cols = parsed_rows.iter().map(|row| row.len()).max().unwrap_or(0);
if max_cols == 0 {
return String::new();
}
let cell_width = if full_names {
parsed_rows.iter().flat_map(|row| row.iter()).map(|token| token.len()).max().unwrap_or(3)
} else {
2 };
let mut output = String::new();
let row_num_width = (grid.len().saturating_sub(1)).to_string().len().max(2);
output.push_str(&" ".repeat(row_num_width + 1)); for col in 0..max_cols {
if full_names {
output.push_str(&format!("{:>width$} ", col, width = cell_width));
} else {
output.push_str(&format!("{:>2} ", col));
}
}
output.push('\n');
output.push_str(&" ".repeat(row_num_width));
output.push_str(" \u{250C}"); let border_width = if full_names { max_cols * (cell_width + 1) } else { max_cols * 3 };
output.push_str(&"\u{2500}".repeat(border_width)); output.push('\n');
for (row_idx, tokens) in parsed_rows.iter().enumerate() {
output.push_str(&format!("{:>width$} \u{2502}", row_idx, width = row_num_width));
for token in tokens {
let display = if full_names {
token.clone()
} else {
let name = token.trim_matches(|c| c == '{' || c == '}');
if name == "_" {
"_".to_string()
} else {
name.chars().next().unwrap_or('?').to_string()
}
};
if full_names {
output.push_str(&format!(" {:>width$}", display, width = cell_width));
} else {
output.push_str(&format!(" {:>2}", display));
}
}
output.push('\n');
}
output
}
pub fn render_image_ansi(image: &image::RgbaImage) -> String {
use image::Rgba;
let width = image.width() as usize;
let height = image.height() as usize;
if width == 0 || height == 0 {
return String::new();
}
let mut output = String::new();
for y in (0..height).step_by(2) {
for x in 0..width {
let top_pixel = *image.get_pixel(x as u32, y as u32);
let bottom_pixel = if y + 1 < height {
*image.get_pixel(x as u32, (y + 1) as u32)
} else {
Rgba([0, 0, 0, 0]) };
if top_pixel[3] == 0 && bottom_pixel[3] == 0 {
output.push_str("\x1b[48;5;236m\x1b[38;5;236m▀");
} else if top_pixel[3] == 0 {
output.push_str(&format!(
"\x1b[48;2;{};{};{}m\x1b[38;5;236m▀",
bottom_pixel[0], bottom_pixel[1], bottom_pixel[2]
));
} else if bottom_pixel[3] == 0 {
output.push_str(&format!(
"\x1b[48;5;236m\x1b[38;2;{};{};{}m▀",
top_pixel[0], top_pixel[1], top_pixel[2]
));
} else {
output.push_str(&format!(
"\x1b[48;2;{};{};{}m\x1b[38;2;{};{};{}m▀",
bottom_pixel[0],
bottom_pixel[1],
bottom_pixel[2],
top_pixel[0],
top_pixel[1],
top_pixel[2]
));
}
}
output.push_str(ANSI_RESET);
output.push('\n');
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_to_ansi_bg_opaque() {
let red = color_to_ansi_bg(Rgba([255, 0, 0, 255]));
assert_eq!(red, "\x1b[48;2;255;0;0m");
let green = color_to_ansi_bg(Rgba([0, 255, 0, 255]));
assert_eq!(green, "\x1b[48;2;0;255;0m");
let blue = color_to_ansi_bg(Rgba([0, 0, 255, 255]));
assert_eq!(blue, "\x1b[48;2;0;0;255m");
}
#[test]
fn test_color_to_ansi_bg_transparent() {
let transparent = color_to_ansi_bg(Rgba([0, 0, 0, 0]));
assert_eq!(transparent, "\x1b[48;5;236m");
let transparent_red = color_to_ansi_bg(Rgba([255, 0, 0, 0]));
assert_eq!(transparent_red, "\x1b[48;5;236m");
}
#[test]
fn test_color_to_ansi_bg_partial_alpha() {
let semi_transparent = color_to_ansi_bg(Rgba([255, 0, 0, 128]));
assert_eq!(semi_transparent, "\x1b[48;2;255;0;0m");
}
#[test]
fn test_render_ansi_grid_simple() {
let grid = vec!["{a}{b}".to_string(), "{b}{a}".to_string()];
let palette = HashMap::from([
("{a}".to_string(), "#FF0000".to_string()),
("{b}".to_string(), "#00FF00".to_string()),
]);
let aliases = HashMap::new();
let (colored, legend) = render_ansi_grid(&grid, &palette, &aliases);
assert!(colored.contains("\x1b[48;2;"));
assert!(colored.contains(ANSI_RESET));
assert!(legend.contains("Legend:"));
assert!(legend.contains("#FF0000") || legend.contains("#00FF00"));
}
#[test]
fn test_render_ansi_grid_with_transparent() {
let grid = vec!["{_}{a}".to_string()];
let palette = HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{a}".to_string(), "#FF0000".to_string()),
]);
let aliases = HashMap::new();
let (colored, _) = render_ansi_grid(&grid, &palette, &aliases);
assert!(colored.contains("\x1b[48;5;236m"));
}
#[test]
fn test_render_coordinate_grid_simple() {
let grid = vec!["{a}{b}{c}".to_string(), "{d}{e}{f}".to_string()];
let output = render_coordinate_grid(&grid, false);
assert!(output.contains(" 0"));
assert!(output.contains(" 1"));
assert!(output.contains(" 2"));
assert!(output.contains("0 \u{2502}")); assert!(output.contains("1 \u{2502}"));
assert!(output.contains(" a"));
assert!(output.contains(" b"));
}
#[test]
fn test_render_coordinate_grid_full_names() {
let grid = vec!["{skin}{hair}".to_string()];
let output = render_coordinate_grid(&grid, true);
assert!(output.contains("{skin}"));
assert!(output.contains("{hair}"));
}
#[test]
fn test_render_coordinate_grid_empty() {
let grid: Vec<String> = vec![];
let output = render_coordinate_grid(&grid, false);
assert!(output.is_empty());
}
#[test]
fn test_render_coordinate_grid_underscore() {
let grid = vec!["{_}{a}{_}".to_string()];
let output = render_coordinate_grid(&grid, false);
assert!(output.contains(" _"));
}
#[test]
fn test_render_image_ansi_empty() {
use image::RgbaImage;
let image = RgbaImage::new(0, 0);
let output = render_image_ansi(&image);
assert!(output.is_empty());
}
#[test]
fn test_render_image_ansi_simple() {
use image::RgbaImage;
let image = RgbaImage::from_pixel(2, 2, Rgba([255, 0, 0, 255]));
let output = render_image_ansi(&image);
assert!(output.contains("\x1b["));
assert!(output.contains("▀"));
assert!(output.contains(ANSI_RESET));
}
#[test]
fn test_render_image_ansi_transparent() {
use image::RgbaImage;
let image = RgbaImage::from_pixel(2, 2, Rgba([0, 0, 0, 0]));
let output = render_image_ansi(&image);
assert!(output.contains("\x1b[48;5;236m"));
}
}