use crate::filter::{Cell, Color, NamedColor, RenderGrid};
const IRC_RESET: u8 = 0x0F;
const IRC_COLOR: u8 = 0x03;
#[must_use]
pub fn write_irc(grid: &RenderGrid, warn_on_strip: bool) -> Vec<u8> {
let w = grid.width as usize;
let h = grid.height as usize;
let capacity = w.saturating_mul(h).saturating_mul(6).saturating_add(64);
let mut out: Vec<u8> = Vec::with_capacity(capacity);
let mut stripped_any = false;
let mut prev_color: Option<Color> = None;
for row in &grid.cells {
for cell in row {
if prev_color != Some(cell.fg) {
out.push(IRC_COLOR);
let palette_idx = color_to_mirc_index(cell.fg);
push_decimal(&mut out, palette_idx);
prev_color = Some(cell.fg);
}
if char_is_irc_safe(cell.ch) {
push_utf8(&mut out, cell.ch);
} else {
stripped_any = true;
}
}
out.push(IRC_RESET);
out.push(b'\n');
prev_color = None;
}
if stripped_any && warn_on_strip {
emit_strip_warning();
}
out
}
fn char_is_irc_safe(c: char) -> bool {
let cp = c as u32;
if c == '\t' {
return true;
}
if cp < 0x20 {
return false;
}
if cp == 0x7F {
return false;
}
if (0x80..=0x9F).contains(&cp) {
return false;
}
true
}
fn color_to_mirc_index(c: Color) -> u8 {
match c {
Color::Named(n) => named_to_mirc(n),
Color::Index(idx) => {
if idx < 16 {
named_to_mirc(palette16_to_named(idx))
} else {
0
}
}
Color::Rgb(_, _, _) => {
0
}
}
}
fn named_to_mirc(n: NamedColor) -> u8 {
match n {
NamedColor::White => 0,
NamedColor::Black => 1,
NamedColor::Blue => 2,
NamedColor::Green => 3,
NamedColor::Red => 4,
NamedColor::BrightRed => 4,
NamedColor::Magenta => 6,
NamedColor::Yellow => 8,
NamedColor::BrightYellow => 8,
NamedColor::BrightGreen => 9,
NamedColor::Cyan => 10,
NamedColor::BrightCyan => 11,
NamedColor::BrightBlue => 12,
NamedColor::BrightMagenta => 13,
NamedColor::BrightBlack => 14,
NamedColor::BrightWhite => 15,
}
}
fn palette16_to_named(idx: u8) -> NamedColor {
match idx {
0 => NamedColor::Black,
1 => NamedColor::Red,
2 => NamedColor::Green,
3 => NamedColor::Yellow,
4 => NamedColor::Blue,
5 => NamedColor::Magenta,
6 => NamedColor::Cyan,
7 => NamedColor::White,
8 => NamedColor::BrightBlack,
9 => NamedColor::BrightRed,
10 => NamedColor::BrightGreen,
11 => NamedColor::BrightYellow,
12 => NamedColor::BrightBlue,
13 => NamedColor::BrightMagenta,
14 => NamedColor::BrightCyan,
_ => NamedColor::BrightWhite,
}
}
fn push_decimal(out: &mut Vec<u8>, n: u8) {
if n >= 10 {
out.push((n / 10) + b'0');
out.push((n % 10) + b'0');
} else {
out.push(n + b'0');
}
}
fn push_utf8(out: &mut Vec<u8>, c: char) {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
out.extend_from_slice(s.as_bytes());
}
#[cold]
#[inline(never)]
fn emit_strip_warning() {
eprintln!("rusty-figlet: IRC export stripped non-printable bytes");
}
#[allow(dead_code)]
fn _suppress_unused(_: Cell) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::{Cell, Color, NamedColor, RenderGrid};
#[test]
fn empty_grid_returns_empty_bytes() {
let grid = RenderGrid::empty();
let out = write_irc(&grid, false);
assert!(out.is_empty());
}
#[test]
fn plain_ascii_row_has_color_prefix_and_reset() {
let grid = RenderGrid::from_text_rows(&[String::from("Hi")]);
let out = write_irc(&grid, false);
assert!(out.contains(&IRC_COLOR));
assert!(out.contains(&IRC_RESET));
assert!(out.contains(&b'H'));
assert!(out.contains(&b'i'));
}
#[test]
fn strips_c0_controls_except_tab() {
let row = vec![
Cell::new('\x00'),
Cell::new('\x01'),
Cell::new('\t'),
Cell::new('A'),
];
let grid = RenderGrid::from_rows(vec![row]);
let out = write_irc(&grid, false);
assert!(out.contains(&b'A'));
assert!(out.contains(&b'\t'));
assert!(!out.contains(&0x00));
assert!(!out.contains(&0x01));
}
#[test]
fn strips_del_byte() {
let grid = RenderGrid::from_rows(vec![vec![Cell::new('\x7F'), Cell::new('B')]]);
let out = write_irc(&grid, false);
assert!(!out.contains(&0x7F));
assert!(out.contains(&b'B'));
}
#[test]
fn strips_c1_range() {
let grid = RenderGrid::from_rows(vec![vec![Cell::new('\u{0085}'), Cell::new('C')]]);
let out = write_irc(&grid, false);
assert!(out.contains(&b'C'));
assert!(!out.contains(&0x85));
}
#[test]
fn preserves_utf8_multibyte_cjk() {
let grid = RenderGrid::from_text_rows(&[String::from("中")]);
let out = write_irc(&grid, false);
assert!(out.contains(&0xE4));
assert!(out.contains(&0xB8));
assert!(out.contains(&0xAD));
}
#[test]
fn preserves_utf8_emoji() {
let grid = RenderGrid::from_text_rows(&[String::from("🦀")]);
let out = write_irc(&grid, false);
assert!(out.contains(&0xF0));
assert!(out.contains(&0x9F));
}
#[test]
fn named_color_maps_to_palette_idx() {
let cell = Cell {
ch: 'X',
fg: Color::Named(NamedColor::Red),
bg: None,
attrs: 0,
};
let grid = RenderGrid::from_rows(vec![vec![cell]]);
let out = write_irc(&grid, false);
let prefix_pos = out.iter().position(|&b| b == IRC_COLOR).expect("has ^C");
assert_eq!(out[prefix_pos + 1], b'4');
}
#[test]
fn truecolor_falls_back_to_white_index() {
let cell = Cell {
ch: 'Y',
fg: Color::Rgb(123, 45, 67),
bg: None,
attrs: 0,
};
let grid = RenderGrid::from_rows(vec![vec![cell]]);
let out = write_irc(&grid, false);
let prefix_pos = out.iter().position(|&b| b == IRC_COLOR).expect("has ^C");
assert_eq!(out[prefix_pos + 1], b'0');
}
#[test]
fn row_ends_with_reset_and_newline() {
let grid = RenderGrid::from_text_rows(&[String::from("A")]);
let out = write_irc(&grid, false);
assert_eq!(out[out.len() - 2], IRC_RESET);
assert_eq!(out[out.len() - 1], b'\n');
}
}