1use crate::cp437;
6use crate::palette::ANSI_TO_VGA;
7use crate::screen::{Cell, Color, DEFAULT_BG, DEFAULT_FG, flags};
8
9pub fn text(lines: &[&[Cell]]) -> String {
11 let mut out: Vec<String> =
12 lines.iter().map(|row| row.iter().map(|c| glyph(c.ch)).collect::<String>().trim_end().to_string()).collect();
13 while out.last().is_some_and(String::is_empty) {
14 out.pop();
15 }
16 let mut s = out.join("\n");
17 if !s.is_empty() {
18 s.push('\n');
19 }
20 s
21}
22
23#[derive(Clone, Copy, Debug, Default)]
25pub struct AnsiOptions {
26 pub ice: bool,
29 pub unicode: bool,
31}
32
33pub fn ansi(lines: &[&[Cell]], opts: AnsiOptions) -> Vec<u8> {
36 let mut out = Vec::new();
37 if opts.ice {
38 out.extend_from_slice(b"\x1b[?33h");
39 }
40 out.extend_from_slice(b"\x1b[0m");
41 let last = lines.iter().rposition(|row| row.iter().any(|c| !blank(c))).map_or(0, |i| i + 1);
42 let default = Cell::default();
43 for row in &lines[..last] {
44 let end = row.iter().rposition(|c| !blank(c)).map_or(0, |i| i + 1);
45 let mut pen = default;
46 for c in &row[..end] {
47 if !same_attrs(c, &pen) {
48 sgr(c, &mut out);
49 pen = *c;
50 }
51 if opts.unicode {
52 let mut buf = [0; 4];
53 out.extend_from_slice(glyph(c.ch).encode_utf8(&mut buf).as_bytes());
54 } else {
55 out.push(safe_byte(c.ch));
56 }
57 }
58 if !same_attrs(&pen, &default) {
59 out.extend_from_slice(b"\x1b[0m");
60 }
61 out.extend_from_slice(b"\r\n");
62 }
63 out
64}
65
66fn glyph(ch: u8) -> char {
67 if ch == 0 { ' ' } else { cp437::to_unicode(ch) }
68}
69
70fn safe_byte(ch: u8) -> u8 {
74 match ch {
75 0 | 7 | 8 | 9 | 10 | 12 | 13 | 26 | 27 => b' ',
76 c => c,
77 }
78}
79
80fn blank(c: &Cell) -> bool {
81 c.is_blank() && c.flags & (flags::UNDERLINE | flags::BLINK) == 0
82}
83
84fn same_attrs(a: &Cell, b: &Cell) -> bool {
85 a.fg == b.fg && a.bg == b.bg && a.flags == b.flags
86}
87
88fn sgr(c: &Cell, out: &mut Vec<u8>) {
90 let mut p: Vec<String> = vec!["0".into()];
91 let f = c.flags;
92 let bright_fg = matches!(c.fg, Color::Pc(i) if i >= 8);
95 for (bit, code) in
96 [(flags::BOLD, "1"), (flags::FAINT, "2"), (flags::UNDERLINE, "4"), (flags::BLINK, "5"), (flags::REVERSE, "7")]
97 {
98 if f & bit != 0 || bit == flags::BOLD && bright_fg {
99 p.push(code.into());
100 }
101 }
102 if f & flags::CONCEAL != 0 {
103 p.push("8".into());
104 }
105 match c.fg {
106 DEFAULT_FG => {}
107 Color::Pc(i) => p.push(format!("{}", 30 + vga_to_ansi(i & 7))),
108 Color::Xterm(i) => p.push(format!("38;5;{i}")),
109 Color::Rgb(rgb) => p.push(format!("38;2;{};{};{}", rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF)),
110 }
111 match c.bg {
112 DEFAULT_BG => {}
113 Color::Pc(i) if i >= 8 => p.push(format!("{}", 100 + vga_to_ansi(i & 7))),
114 Color::Pc(i) => p.push(format!("{}", 40 + vga_to_ansi(i))),
115 Color::Xterm(i) => p.push(format!("48;5;{i}")),
116 Color::Rgb(rgb) => p.push(format!("48;2;{};{};{}", rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF)),
117 }
118 out.extend_from_slice(format!("\x1b[{}m", p.join(";")).as_bytes());
119}
120
121fn vga_to_ansi(i: u8) -> u8 {
122 ANSI_TO_VGA[i as usize & 7]
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::ansi::Terminal;
131
132 fn rows(t: &Terminal) -> Vec<&[Cell]> {
133 (0..t.screen.rows()).map(|y| t.screen.row(y)).collect()
134 }
135
136 #[test]
137 fn plain_text() {
138 let mut t = Terminal::new(20, 5, 0);
139 t.feed(b"\x1b[1;31mHello\x1b[0m \r\n\r\n\xc9\xcd\xbb\r\n");
140 assert_eq!(text(&rows(&t)), "Hello\n\n\u{2554}\u{2550}\u{2557}\n");
141 }
142
143 #[test]
144 fn ansi_round_trips_through_the_terminal() {
145 let mut t = Terminal::new(40, 6, 0);
146 t.feed(b"\x1b[1;33;44mBold yellow on blue\x1b[0m plain\r\n");
147 t.feed(b"\x1b[5;31;47mblink\x1b[0m \x1b[38;5;196mxterm\x1b[38;2;1;2;3m rgb\x1b[0m\r\n");
148 t.feed(b"\x1b[92mbright\x1b[0m \x1b[104mbg\x1b[0m \x1b[4munder\x1b[7mrev\x1b[0m\r\n");
149 t.feed(b"\x1b[41m \x1b[0m\x1b[30C\xdb\x1b[K");
150 let data = ansi(&rows(&t), AnsiOptions::default());
151
152 let mut back = Terminal::new(40, 6, 0);
153 back.feed(&data);
154 for y in 0..6 {
155 for x in 0..40 {
156 let (a, b) = (t.screen.row(y)[x], back.screen.row(y)[x]);
157 assert_eq!(a.ch, b.ch, "glyph at {x},{y}");
158 assert_eq!(a.resolve(false), b.resolve(false), "colors at {x},{y}");
159 }
160 }
161 }
162
163 #[test]
164 fn options_and_safety() {
165 let mut t = Terminal::new(10, 2, 0);
166 t.screen.put(0x1B);
167 t.screen.put(0x01);
168 let raw = ansi(&rows(&t), AnsiOptions { ice: true, unicode: false });
169 assert_eq!(raw, b"\x1b[?33h\x1b[0m \x01\r\n");
170 let uni = ansi(&rows(&t), AnsiOptions { ice: false, unicode: true });
171 assert_eq!(String::from_utf8(uni).unwrap(), "\x1b[0m\u{2190}\u{263A}\r\n");
172 assert_eq!(ansi(&rows(&Terminal::new(10, 2, 0)), AnsiOptions::default()), b"\x1b[0m");
174 }
175}