rusty_figlet/export/
irc.rs1use crate::filter::{Cell, Color, NamedColor, RenderGrid};
40
41const IRC_RESET: u8 = 0x0F;
43const IRC_COLOR: u8 = 0x03;
45
46#[must_use]
53pub fn write_irc(grid: &RenderGrid, warn_on_strip: bool) -> Vec<u8> {
54 let w = grid.width as usize;
55 let h = grid.height as usize;
56 let capacity = w.saturating_mul(h).saturating_mul(6).saturating_add(64);
57 let mut out: Vec<u8> = Vec::with_capacity(capacity);
58 let mut stripped_any = false;
59 let mut prev_color: Option<Color> = None;
60
61 for row in &grid.cells {
62 for cell in row {
63 if prev_color != Some(cell.fg) {
67 out.push(IRC_COLOR);
68 let palette_idx = color_to_mirc_index(cell.fg);
69 push_decimal(&mut out, palette_idx);
70 prev_color = Some(cell.fg);
71 }
72 if char_is_irc_safe(cell.ch) {
74 push_utf8(&mut out, cell.ch);
75 } else {
76 stripped_any = true;
77 }
78 }
79 out.push(IRC_RESET);
81 out.push(b'\n');
82 prev_color = None;
83 }
84
85 if stripped_any && warn_on_strip {
86 emit_strip_warning();
87 }
88
89 out
90}
91
92fn char_is_irc_safe(c: char) -> bool {
99 let cp = c as u32;
100 if c == '\t' {
101 return true;
102 }
103 if cp < 0x20 {
104 return false;
105 }
106 if cp == 0x7F {
107 return false;
108 }
109 if (0x80..=0x9F).contains(&cp) {
110 return false;
111 }
112 true
113}
114
115fn color_to_mirc_index(c: Color) -> u8 {
121 match c {
122 Color::Named(n) => named_to_mirc(n),
123 Color::Index(idx) => {
124 if idx < 16 {
127 named_to_mirc(palette16_to_named(idx))
128 } else {
129 0
130 }
131 }
132 Color::Rgb(_, _, _) => {
133 0
137 }
138 }
139}
140
141fn named_to_mirc(n: NamedColor) -> u8 {
143 match n {
144 NamedColor::White => 0,
145 NamedColor::Black => 1,
146 NamedColor::Blue => 2,
147 NamedColor::Green => 3,
148 NamedColor::Red => 4,
149 NamedColor::BrightRed => 4,
150 NamedColor::Magenta => 6,
151 NamedColor::Yellow => 8,
152 NamedColor::BrightYellow => 8,
153 NamedColor::BrightGreen => 9,
154 NamedColor::Cyan => 10,
155 NamedColor::BrightCyan => 11,
156 NamedColor::BrightBlue => 12,
157 NamedColor::BrightMagenta => 13,
158 NamedColor::BrightBlack => 14,
159 NamedColor::BrightWhite => 15,
160 }
161}
162
163fn palette16_to_named(idx: u8) -> NamedColor {
164 match idx {
165 0 => NamedColor::Black,
166 1 => NamedColor::Red,
167 2 => NamedColor::Green,
168 3 => NamedColor::Yellow,
169 4 => NamedColor::Blue,
170 5 => NamedColor::Magenta,
171 6 => NamedColor::Cyan,
172 7 => NamedColor::White,
173 8 => NamedColor::BrightBlack,
174 9 => NamedColor::BrightRed,
175 10 => NamedColor::BrightGreen,
176 11 => NamedColor::BrightYellow,
177 12 => NamedColor::BrightBlue,
178 13 => NamedColor::BrightMagenta,
179 14 => NamedColor::BrightCyan,
180 _ => NamedColor::BrightWhite,
181 }
182}
183
184fn push_decimal(out: &mut Vec<u8>, n: u8) {
185 if n >= 10 {
186 out.push((n / 10) + b'0');
187 out.push((n % 10) + b'0');
188 } else {
189 out.push(n + b'0');
192 }
193}
194
195fn push_utf8(out: &mut Vec<u8>, c: char) {
196 let mut buf = [0u8; 4];
197 let s = c.encode_utf8(&mut buf);
198 out.extend_from_slice(s.as_bytes());
199}
200
201#[cold]
202#[inline(never)]
203fn emit_strip_warning() {
204 eprintln!("rusty-figlet: IRC export stripped non-printable bytes");
205}
206
207#[allow(dead_code)]
208fn _suppress_unused(_: Cell) {}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::filter::{Cell, Color, NamedColor, RenderGrid};
214
215 #[test]
216 fn empty_grid_returns_empty_bytes() {
217 let grid = RenderGrid::empty();
218 let out = write_irc(&grid, false);
219 assert!(out.is_empty());
220 }
221
222 #[test]
223 fn plain_ascii_row_has_color_prefix_and_reset() {
224 let grid = RenderGrid::from_text_rows(&[String::from("Hi")]);
225 let out = write_irc(&grid, false);
226 assert!(out.contains(&IRC_COLOR));
227 assert!(out.contains(&IRC_RESET));
228 assert!(out.contains(&b'H'));
229 assert!(out.contains(&b'i'));
230 }
231
232 #[test]
233 fn strips_c0_controls_except_tab() {
234 let row = vec![
235 Cell::new('\x00'),
236 Cell::new('\x01'),
237 Cell::new('\t'),
238 Cell::new('A'),
239 ];
240 let grid = RenderGrid::from_rows(vec![row]);
241 let out = write_irc(&grid, false);
242 assert!(out.contains(&b'A'));
244 assert!(out.contains(&b'\t'));
245 assert!(!out.contains(&0x00));
246 assert!(!out.contains(&0x01));
247 }
248
249 #[test]
250 fn strips_del_byte() {
251 let grid = RenderGrid::from_rows(vec![vec![Cell::new('\x7F'), Cell::new('B')]]);
252 let out = write_irc(&grid, false);
253 assert!(!out.contains(&0x7F));
254 assert!(out.contains(&b'B'));
255 }
256
257 #[test]
258 fn strips_c1_range() {
259 let grid = RenderGrid::from_rows(vec![vec![Cell::new('\u{0085}'), Cell::new('C')]]);
261 let out = write_irc(&grid, false);
262 assert!(out.contains(&b'C'));
263 assert!(!out.contains(&0x85));
265 }
266
267 #[test]
268 fn preserves_utf8_multibyte_cjk() {
269 let grid = RenderGrid::from_text_rows(&[String::from("中")]);
270 let out = write_irc(&grid, false);
271 assert!(out.contains(&0xE4));
273 assert!(out.contains(&0xB8));
274 assert!(out.contains(&0xAD));
275 }
276
277 #[test]
278 fn preserves_utf8_emoji() {
279 let grid = RenderGrid::from_text_rows(&[String::from("🦀")]);
280 let out = write_irc(&grid, false);
281 assert!(out.contains(&0xF0));
283 assert!(out.contains(&0x9F));
284 }
285
286 #[test]
287 fn named_color_maps_to_palette_idx() {
288 let cell = Cell {
289 ch: 'X',
290 fg: Color::Named(NamedColor::Red),
291 bg: None,
292 attrs: 0,
293 };
294 let grid = RenderGrid::from_rows(vec![vec![cell]]);
295 let out = write_irc(&grid, false);
296 let prefix_pos = out.iter().position(|&b| b == IRC_COLOR).expect("has ^C");
298 assert_eq!(out[prefix_pos + 1], b'4');
299 }
300
301 #[test]
302 fn truecolor_falls_back_to_white_index() {
303 let cell = Cell {
304 ch: 'Y',
305 fg: Color::Rgb(123, 45, 67),
306 bg: None,
307 attrs: 0,
308 };
309 let grid = RenderGrid::from_rows(vec![vec![cell]]);
310 let out = write_irc(&grid, false);
311 let prefix_pos = out.iter().position(|&b| b == IRC_COLOR).expect("has ^C");
312 assert_eq!(out[prefix_pos + 1], b'0');
313 }
314
315 #[test]
316 fn row_ends_with_reset_and_newline() {
317 let grid = RenderGrid::from_text_rows(&[String::from("A")]);
318 let out = write_irc(&grid, false);
319 assert_eq!(out[out.len() - 2], IRC_RESET);
320 assert_eq!(out[out.len() - 1], b'\n');
321 }
322}