1use turbo_vision::core::ansi::AnsiParser;
7use turbo_vision::core::draw::Cell;
8use turbo_vision::core::palette::{Attr, Style, TvColor};
9
10const ERASE_LINE: &[u8] = b"\r\x1b[0K";
14
15#[must_use]
22pub fn attr_to_sgr(attr: Attr) -> String {
23 fn ansi256_index(color: TvColor) -> u8 {
30 match color {
31 TvColor::Black | TvColor::Rgb { .. } => 0,
32 TvColor::Red => 1,
33 TvColor::Green => 2,
34 TvColor::Brown => 3,
35 TvColor::Blue => 4,
36 TvColor::Magenta => 5,
37 TvColor::Cyan => 6,
38 TvColor::LightGray => 7,
39 TvColor::DarkGray => 8,
40 TvColor::LightRed => 9,
41 TvColor::LightGreen => 10,
42 TvColor::Yellow => 11,
43 TvColor::LightBlue => 12,
44 TvColor::LightMagenta => 13,
45 TvColor::LightCyan => 14,
46 TvColor::White => 15,
47 }
48 }
49 fn one(kind: u8, color: TvColor) -> String {
50 match color {
51 TvColor::Rgb { r, g, b } => format!("\x1b[{kind};2;{r};{g};{b}m"),
52 other => format!("\x1b[{kind};5;{}m", ansi256_index(other)),
53 }
54 }
55 use std::fmt::Write as _;
61 let mut style = String::new();
62 for (flag, code) in [
63 (Style::BOLD, 1),
64 (Style::DIM, 2),
65 (Style::ITALIC, 3),
66 (Style::UNDERLINE, 4),
67 (Style::REVERSE, 7),
68 (Style::STRIKETHROUGH, 9),
69 ] {
70 if attr.style.contains(flag) {
71 let _ = write!(style, "\x1b[{code}m");
72 }
73 }
74 format!("{}{}{}", one(38, attr.fg), one(48, attr.bg), style)
75}
76
77pub struct AnsiLineAssembler {
94 parser: AnsiParser,
95 pending: Vec<u8>,
97 carry: Attr,
99 ready: Vec<Vec<Cell>>,
101}
102
103impl std::fmt::Debug for AnsiLineAssembler {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("AnsiLineAssembler")
108 .field("pending_len", &self.pending.len())
109 .field("carry", &self.carry)
110 .field("ready_len", &self.ready.len())
111 .finish_non_exhaustive()
112 }
113}
114
115impl AnsiLineAssembler {
116 #[must_use]
117 pub fn new() -> Self {
118 Self {
119 parser: AnsiParser::new(),
120 pending: Vec::new(),
121 carry: Attr::new(TvColor::LightGray, TvColor::Black),
122 ready: Vec::new(),
123 }
124 }
125
126 pub fn push(&mut self, bytes: &[u8]) {
128 for &b in bytes {
129 if b == b'\n' {
130 let line = self.parse_pending();
131 self.carry = self.trailing_attr_of_pending();
132 self.ready.push(line);
133 self.pending.clear();
134 } else {
135 self.pending.push(b);
136 if self.pending.ends_with(ERASE_LINE) {
137 self.pending.clear();
148 }
149 }
150 }
151 }
152
153 pub fn take_complete_lines(&mut self) -> Vec<Vec<Cell>> {
155 std::mem::take(&mut self.ready)
156 }
157
158 #[must_use]
163 pub fn partial_line(&self) -> Vec<Cell> {
164 self.parse_pending()
165 }
166
167 pub fn flush(&mut self) -> Option<Vec<Cell>> {
170 if self.pending.is_empty() {
171 return None;
172 }
173 let line = self.parse_pending();
174 self.carry = self.trailing_attr_of_pending();
175 self.pending.clear();
176 Some(line)
177 }
178
179 fn parse_pending(&self) -> Vec<Cell> {
182 let usable = &self.pending[..complete_len(&self.pending)];
183 let text = String::from_utf8_lossy(usable);
184 let with_state = format!("{}{}", attr_to_sgr(self.carry), text);
185 self.parser.parse_line(&with_state)
186 }
187
188 fn trailing_attr_of_pending(&self) -> Attr {
191 let usable = &self.pending[..complete_len(&self.pending)];
192 let text = String::from_utf8_lossy(usable);
193 let probe = format!("{}{}X", attr_to_sgr(self.carry), text);
194 self.parser
195 .parse_line(&probe)
196 .last()
197 .map_or(self.carry, |c| c.attr)
198 }
199}
200
201impl Default for AnsiLineAssembler {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207fn complete_len(buf: &[u8]) -> usize {
213 let Some(esc) = buf.iter().rposition(|&b| b == 0x1b) else {
214 return buf.len();
215 };
216 let tail = &buf[esc..];
217 if tail.len() == 1 {
219 return esc;
220 }
221 if tail[1] != b'[' {
222 return buf.len();
224 }
225 if tail[2..].iter().any(|&b| (0x40..=0x7e).contains(&b)) {
226 buf.len()
227 } else {
228 esc
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use turbo_vision::core::palette::TvColor;
236
237 fn text(cells: &[Cell]) -> String {
238 cells.iter().map(|c| c.ch).collect()
239 }
240
241 #[test]
242 fn splits_on_newline_and_holds_the_tail() {
243 let mut a = AnsiLineAssembler::new();
244 a.push(b"one\ntwo");
245 let lines = a.take_complete_lines();
246 assert_eq!(lines.len(), 1);
247 assert_eq!(text(&lines[0]), "one");
248 assert_eq!(text(&a.partial_line()), "two");
249 }
250
251 #[test]
252 fn attribute_carries_across_a_line_break() {
253 let mut a = AnsiLineAssembler::new();
254 a.push(b"\x1b[31mred one\nstill red");
255 let lines = a.take_complete_lines();
256 assert_eq!(lines[0].last().unwrap().attr.fg, TvColor::Red);
257 let partial = a.partial_line();
258 assert_eq!(
259 partial[0].attr.fg,
260 TvColor::Red,
261 "SGR state must survive the newline"
262 );
263 }
264
265 #[test]
266 fn text_style_carries_across_a_line_break() {
267 use turbo_vision::core::palette::Style;
268 let mut a = AnsiLineAssembler::new();
269 a.push(b"\x1b[1;3mstyled one\nstill styled");
271 let lines = a.take_complete_lines();
272 assert!(lines[0].last().unwrap().attr.style.contains(Style::BOLD));
273 let partial = a.partial_line();
274 assert!(
275 partial[0].attr.style.contains(Style::BOLD),
276 "bold must survive the newline"
277 );
278 assert!(
279 partial[0].attr.style.contains(Style::ITALIC),
280 "italic must survive the newline"
281 );
282 }
283
284 #[test]
285 fn byte_at_a_time_matches_whole_delivery() {
286 let input = b"\x1b[1;32mgreen\x1b[0m plain\nnext\n";
287 let mut whole = AnsiLineAssembler::new();
288 whole.push(input);
289 let expected = whole.take_complete_lines();
290
291 let mut drip = AnsiLineAssembler::new();
292 let mut got = Vec::new();
293 for b in input {
294 drip.push(&[*b]);
295 got.extend(drip.take_complete_lines());
296 }
297 assert_eq!(got, expected);
298 }
299
300 #[test]
301 fn escape_split_across_chunks_is_not_shown_as_text() {
302 let mut a = AnsiLineAssembler::new();
303 a.push(b"x\x1b[3");
304 assert_eq!(
305 text(&a.partial_line()),
306 "x",
307 "an incomplete escape must not leak as literal characters"
308 );
309 a.push(b"1mY");
310 assert_eq!(text(&a.partial_line()), "xY");
311 assert_eq!(a.partial_line()[1].attr.fg, TvColor::Red);
312 }
313
314 #[test]
315 fn carriage_return_is_dropped_not_rendered() {
316 let mut a = AnsiLineAssembler::new();
317 a.push(b"abc\r\n");
318 let lines = a.take_complete_lines();
319 assert_eq!(text(&lines[0]), "abc");
320 }
321
322 #[test]
323 fn fence_repaint_replaces_the_line_instead_of_appending_to_it() {
324 let mut a = AnsiLineAssembler::new();
334 a.push(b"fn main() {}\x1b[0m\r\x1b[0K\x1b[38;5;214mfn\x1b[0m main() {}\n");
335 let lines = a.take_complete_lines();
336 assert_eq!(
337 text(&lines[0]),
338 "fn main() {}",
339 "the plain pre-repaint text must not survive alongside the repaint"
340 );
341 assert_ne!(
342 lines[0][0].attr.fg,
343 TvColor::LightGray,
344 "the repainted line must carry the highlight color, not the default"
345 );
346 }
347
348 #[test]
349 fn trailing_sgr_after_the_last_char_does_not_bleed_into_the_next_line() {
350 let mut a = AnsiLineAssembler::new();
356 a.push(b"\x1b[38;5;8mpondering\x1b[0m\nplain text\n");
357 let lines = a.take_complete_lines();
358 assert_eq!(
359 lines[1][0].attr.fg,
360 TvColor::LightGray,
361 "the reset after the last char of line 0 must carry into line 1, \
362 not line 0's last cell color"
363 );
364 }
365
366 #[test]
367 fn flush_emits_a_trailing_line_without_a_newline() {
368 let mut a = AnsiLineAssembler::new();
369 a.push(b"tail");
370 assert_eq!(text(&a.flush().unwrap()), "tail");
371 assert!(a.flush().is_none(), "flush must be idempotent");
372 }
373}