1use crate::codepage::CodePage;
6use crate::cp437;
7use crate::palette::ANSI_TO_VGA;
8use crate::screen::{Cell, Color, DEFAULT_BG, DEFAULT_FG, flags};
9
10pub fn text(lines: &[&[Cell]]) -> String {
12 text_in(lines, None)
13}
14
15pub fn text_in(lines: &[&[Cell]], codepage: Option<&CodePage>) -> String {
17 let mut out: Vec<String> = lines
18 .iter()
19 .map(|row| row.iter().map(|c| glyph(c.ch, codepage)).collect::<String>().trim_end().to_string())
20 .collect();
21 while out.last().is_some_and(String::is_empty) {
22 out.pop();
23 }
24 let mut s = out.join("\n");
25 if !s.is_empty() {
26 s.push('\n');
27 }
28 s
29}
30
31#[derive(Clone, Copy, Debug, Default)]
33pub struct AnsiOptions {
34 pub ice: bool,
37 pub unicode: bool,
39 pub codepage: Option<&'static CodePage>,
41}
42
43pub fn ansi(lines: &[&[Cell]], opts: AnsiOptions) -> Vec<u8> {
46 let mut out = Vec::new();
47 if opts.ice {
48 out.extend_from_slice(b"\x1b[?33h");
49 }
50 out.extend_from_slice(b"\x1b[0m");
51 let last = lines.iter().rposition(|row| row.iter().any(|c| !blank(c))).map_or(0, |i| i + 1);
52 let default = Cell::default();
53 for row in &lines[..last] {
54 let end = row.iter().rposition(|c| !blank(c)).map_or(0, |i| i + 1);
55 let mut pen = default;
56 for c in &row[..end] {
57 if !same_attrs(c, &pen) {
58 sgr(c, &mut out);
59 pen = *c;
60 }
61 if opts.unicode {
62 let mut buf = [0; 4];
63 out.extend_from_slice(glyph(c.ch, opts.codepage).encode_utf8(&mut buf).as_bytes());
64 } else {
65 out.push(safe_byte(c.ch));
66 }
67 }
68 if !same_attrs(&pen, &default) {
69 out.extend_from_slice(b"\x1b[0m");
70 }
71 out.extend_from_slice(b"\r\n");
72 }
73 out
74}
75
76fn glyph(ch: u8, codepage: Option<&CodePage>) -> char {
79 match codepage.map(|cp| cp.decode(ch)) {
80 _ if ch == 0 => ' ',
81 Some(c) if ch >= 0x80 && !c.is_control() && c != '\u{fffd}' => c,
82 _ => cp437::to_unicode(ch),
83 }
84}
85
86fn safe_byte(ch: u8) -> u8 {
90 match ch {
91 0 | 7 | 8 | 9 | 10 | 12 | 13 | 26 | 27 => b' ',
92 c => c,
93 }
94}
95
96fn blank(c: &Cell) -> bool {
97 c.is_blank() && c.flags & (flags::UNDERLINE | flags::BLINK) == 0
98}
99
100fn same_attrs(a: &Cell, b: &Cell) -> bool {
101 a.fg == b.fg && a.bg == b.bg && a.flags == b.flags
102}
103
104fn sgr(c: &Cell, out: &mut Vec<u8>) {
106 let mut p: Vec<String> = vec!["0".into()];
107 let f = c.flags;
108 let bright_fg = matches!(c.fg, Color::Pc(i) if i >= 8);
111 for (bit, code) in
112 [(flags::BOLD, "1"), (flags::FAINT, "2"), (flags::UNDERLINE, "4"), (flags::BLINK, "5"), (flags::REVERSE, "7")]
113 {
114 if f & bit != 0 || bit == flags::BOLD && bright_fg {
115 p.push(code.into());
116 }
117 }
118 if f & flags::CONCEAL != 0 {
119 p.push("8".into());
120 }
121 match c.fg {
122 DEFAULT_FG => {}
123 Color::Pc(i) => p.push(format!("{}", 30 + vga_to_ansi(i & 7))),
124 Color::Xterm(i) => p.push(format!("38;5;{i}")),
125 Color::Rgb(rgb) => p.push(format!("38;2;{};{};{}", rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF)),
126 }
127 match c.bg {
128 DEFAULT_BG => {}
129 Color::Pc(i) if i >= 8 => p.push(format!("{}", 100 + vga_to_ansi(i & 7))),
130 Color::Pc(i) => p.push(format!("{}", 40 + vga_to_ansi(i))),
131 Color::Xterm(i) => p.push(format!("48;5;{i}")),
132 Color::Rgb(rgb) => p.push(format!("48;2;{};{};{}", rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF)),
133 }
134 out.extend_from_slice(format!("\x1b[{}m", p.join(";")).as_bytes());
135}
136
137fn vga_to_ansi(i: u8) -> u8 {
138 ANSI_TO_VGA[i as usize & 7]
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::ansi::Terminal;
147
148 fn rows(t: &Terminal) -> Vec<&[Cell]> {
149 (0..t.screen.rows()).map(|y| t.screen.row(y)).collect()
150 }
151
152 #[test]
153 fn plain_text() {
154 let mut t = Terminal::new(20, 5, 0);
155 t.feed(b"\x1b[1;31mHello\x1b[0m \r\n\r\n\xc9\xcd\xbb\r\n");
156 assert_eq!(text(&rows(&t)), "Hello\n\n\u{2554}\u{2550}\u{2557}\n");
157 }
158
159 #[test]
160 fn ansi_round_trips_through_the_terminal() {
161 let mut t = Terminal::new(40, 6, 0);
162 t.feed(b"\x1b[1;33;44mBold yellow on blue\x1b[0m plain\r\n");
163 t.feed(b"\x1b[5;31;47mblink\x1b[0m \x1b[38;5;196mxterm\x1b[38;2;1;2;3m rgb\x1b[0m\r\n");
164 t.feed(b"\x1b[92mbright\x1b[0m \x1b[104mbg\x1b[0m \x1b[4munder\x1b[7mrev\x1b[0m\r\n");
165 t.feed(b"\x1b[41m \x1b[0m\x1b[30C\xdb\x1b[K");
166 let data = ansi(&rows(&t), AnsiOptions::default());
167
168 let mut back = Terminal::new(40, 6, 0);
169 back.feed(&data);
170 for y in 0..6 {
171 for x in 0..40 {
172 let (a, b) = (t.screen.row(y)[x], back.screen.row(y)[x]);
173 assert_eq!(a.ch, b.ch, "glyph at {x},{y}");
174 assert_eq!(a.resolve(false), b.resolve(false), "colors at {x},{y}");
175 }
176 }
177 }
178
179 #[test]
180 fn options_and_safety() {
181 let mut t = Terminal::new(10, 2, 0);
182 t.screen.put(0x1B);
183 t.screen.put(0x01);
184 let raw = ansi(&rows(&t), AnsiOptions { ice: true, unicode: false, codepage: None });
185 assert_eq!(raw, b"\x1b[?33h\x1b[0m \x01\r\n");
186 let uni = ansi(&rows(&t), AnsiOptions { ice: false, unicode: true, codepage: None });
187 assert_eq!(String::from_utf8(uni).unwrap(), "\x1b[0m\u{2190}\u{263A}\r\n");
188 assert_eq!(ansi(&rows(&Terminal::new(10, 2, 0)), AnsiOptions::default()), b"\x1b[0m");
190 }
191
192 #[test]
193 fn copying_from_a_code_page() {
194 let mut t = Terminal::new(20, 2, 0);
195 t.feed(&[0x8F, 0xE0, 0xA8, 0xA2, 0xA5, 0xE2, b' ', 0xC9, 0xCD]);
196 let cp866 = crate::codepage::for_font(25);
197 assert_eq!(text_in(&rows(&t), cp866), "Привет ╔═\n");
198 let mut t = Terminal::new(4, 1, 0);
200 t.feed(&[0x81, 0xE9]);
201 assert_eq!(text_in(&rows(&t), crate::codepage::for_font(24)), "üé\n");
202 }
203}