1use ratatui::backend::{Backend, ClearType, WindowSize};
7use ratatui::buffer::Cell;
8use ratatui::layout::{Position, Size};
9use ratatui::style::{Color, Modifier};
10use std::io::{self, Write};
11
12pub struct CaptureBackend {
14 buffer: Vec<u8>,
16 size: Size,
18 cursor: Position,
20 cursor_visible: bool,
22 current_fg: Color,
24 current_bg: Color,
25 current_modifiers: Modifier,
26}
27
28impl CaptureBackend {
29 pub fn new(cols: u16, rows: u16) -> Self {
31 Self {
32 buffer: Vec::with_capacity(16 * 1024), size: Size::new(cols, rows),
34 cursor: Position::new(0, 0),
35 cursor_visible: true,
36 current_fg: Color::Reset,
37 current_bg: Color::Reset,
38 current_modifiers: Modifier::empty(),
39 }
40 }
41
42 pub fn take_buffer(&mut self) -> Vec<u8> {
44 std::mem::take(&mut self.buffer)
45 }
46
47 pub fn get_buffer(&self) -> &[u8] {
49 &self.buffer
50 }
51
52 pub fn clear_buffer(&mut self) {
54 self.buffer.clear();
55 }
56
57 pub fn resize(&mut self, cols: u16, rows: u16) {
59 self.size = Size::new(cols, rows);
60 }
61
62 pub fn reset_style_state(&mut self) {
65 self.current_fg = Color::Reset;
66 self.current_bg = Color::Reset;
67 self.current_modifiers = Modifier::empty();
68 }
69
70 fn write_cursor_position(&mut self, x: u16, y: u16) {
72 write!(self.buffer, "\x1b[{};{}H", y + 1, x + 1).unwrap();
74 self.cursor = Position::new(x, y);
75 }
76
77 fn write_style(&mut self, cell: &Cell) {
79 let mut needs_reset = false;
80 let mut sgr_params = Vec::new();
81
82 if cell.modifier != self.current_modifiers {
84 let removed = self.current_modifiers - cell.modifier;
86 if !removed.is_empty() {
87 needs_reset = true;
88 }
89 }
90
91 if needs_reset {
92 sgr_params.push(0);
93 self.current_fg = Color::Reset;
94 self.current_bg = Color::Reset;
95 self.current_modifiers = Modifier::empty();
96 }
97
98 if cell.modifier.contains(Modifier::BOLD)
100 && !self.current_modifiers.contains(Modifier::BOLD)
101 {
102 sgr_params.push(1);
103 }
104 if cell.modifier.contains(Modifier::DIM) && !self.current_modifiers.contains(Modifier::DIM)
105 {
106 sgr_params.push(2);
107 }
108 if cell.modifier.contains(Modifier::ITALIC)
109 && !self.current_modifiers.contains(Modifier::ITALIC)
110 {
111 sgr_params.push(3);
112 }
113 if cell.modifier.contains(Modifier::UNDERLINED)
114 && !self.current_modifiers.contains(Modifier::UNDERLINED)
115 {
116 sgr_params.push(4);
117 }
118 if cell.modifier.contains(Modifier::SLOW_BLINK)
119 && !self.current_modifiers.contains(Modifier::SLOW_BLINK)
120 {
121 sgr_params.push(5);
122 }
123 if cell.modifier.contains(Modifier::RAPID_BLINK)
124 && !self.current_modifiers.contains(Modifier::RAPID_BLINK)
125 {
126 sgr_params.push(6);
127 }
128 if cell.modifier.contains(Modifier::REVERSED)
129 && !self.current_modifiers.contains(Modifier::REVERSED)
130 {
131 sgr_params.push(7);
132 }
133 if cell.modifier.contains(Modifier::HIDDEN)
134 && !self.current_modifiers.contains(Modifier::HIDDEN)
135 {
136 sgr_params.push(8);
137 }
138 if cell.modifier.contains(Modifier::CROSSED_OUT)
139 && !self.current_modifiers.contains(Modifier::CROSSED_OUT)
140 {
141 sgr_params.push(9);
142 }
143
144 if cell.fg != self.current_fg {
146 self.write_color_params(&mut sgr_params, cell.fg, true);
147 }
148
149 if cell.bg != self.current_bg {
151 self.write_color_params(&mut sgr_params, cell.bg, false);
152 }
153
154 if !sgr_params.is_empty() {
156 self.buffer.extend_from_slice(b"\x1b[");
157 for (i, param) in sgr_params.iter().enumerate() {
158 if i > 0 {
159 self.buffer.push(b';');
160 }
161 write!(self.buffer, "{}", param).unwrap();
162 }
163 self.buffer.push(b'm');
164 }
165
166 self.current_fg = cell.fg;
167 self.current_bg = cell.bg;
168 self.current_modifiers = cell.modifier;
169 }
170
171 fn write_color_params(&self, params: &mut Vec<u8>, color: Color, foreground: bool) {
173 let base = if foreground { 30 } else { 40 };
174
175 match color {
176 Color::Reset => params.push(if foreground { 39 } else { 49 }),
177 Color::Black => params.push(base),
178 Color::Red => params.push(base + 1),
179 Color::Green => params.push(base + 2),
180 Color::Yellow => params.push(base + 3),
181 Color::Blue => params.push(base + 4),
182 Color::Magenta => params.push(base + 5),
183 Color::Cyan => params.push(base + 6),
184 Color::Gray => params.push(base + 7),
185 Color::DarkGray => params.push(base + 60),
186 Color::LightRed => params.push(base + 61),
187 Color::LightGreen => params.push(base + 62),
188 Color::LightYellow => params.push(base + 63),
189 Color::LightBlue => params.push(base + 64),
190 Color::LightMagenta => params.push(base + 65),
191 Color::LightCyan => params.push(base + 66),
192 Color::White => params.push(base + 67),
193 Color::Indexed(i) => {
194 params.push(if foreground { 38 } else { 48 });
195 params.push(5);
196 params.push(i);
197 }
198 Color::Rgb(r, g, b) => {
199 params.push(if foreground { 38 } else { 48 });
200 params.push(2);
201 params.push(r);
202 params.push(g);
203 params.push(b);
204 }
205 }
206 }
207}
208
209impl Backend for CaptureBackend {
210 type Error = io::Error;
211
212 fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
213 where
214 I: Iterator<Item = (u16, u16, &'a Cell)>,
215 {
216 let mut last_pos: Option<(u16, u16)> = None;
217
218 for (x, y, cell) in content {
219 let needs_move = match last_pos {
221 None => true,
222 Some((lx, ly)) => {
223 !(ly == y && lx + 1 == x)
225 }
226 };
227
228 if needs_move {
229 self.write_cursor_position(x, y);
230 }
231
232 self.write_style(cell);
234
235 let symbol = cell.symbol();
237 self.buffer.extend_from_slice(symbol.as_bytes());
238
239 last_pos = Some((x, y));
240 }
241
242 Ok(())
243 }
244
245 fn hide_cursor(&mut self) -> io::Result<()> {
246 self.buffer.extend_from_slice(b"\x1b[?25l");
249 self.cursor_visible = false;
250 Ok(())
251 }
252
253 fn show_cursor(&mut self) -> io::Result<()> {
254 self.buffer.extend_from_slice(b"\x1b[?25h");
256 self.cursor_visible = true;
257 Ok(())
258 }
259
260 fn get_cursor_position(&mut self) -> io::Result<Position> {
261 Ok(self.cursor)
262 }
263
264 fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
265 let pos = position.into();
266 self.write_cursor_position(pos.x, pos.y);
267 Ok(())
268 }
269
270 fn clear(&mut self) -> io::Result<()> {
271 self.buffer.extend_from_slice(b"\x1b[2J");
273 self.buffer.extend_from_slice(b"\x1b[H");
275 self.cursor = Position::new(0, 0);
276 Ok(())
277 }
278
279 fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
280 match clear_type {
281 ClearType::All => {
282 self.buffer.extend_from_slice(b"\x1b[2J");
283 }
284 ClearType::AfterCursor => {
285 self.buffer.extend_from_slice(b"\x1b[J");
286 }
287 ClearType::BeforeCursor => {
288 self.buffer.extend_from_slice(b"\x1b[1J");
289 }
290 ClearType::CurrentLine => {
291 self.buffer.extend_from_slice(b"\x1b[2K");
292 }
293 ClearType::UntilNewLine => {
294 self.buffer.extend_from_slice(b"\x1b[K");
295 }
296 }
297 Ok(())
298 }
299
300 fn append_lines(&mut self, n: u16) -> io::Result<()> {
301 for _ in 0..n {
303 self.buffer.extend_from_slice(b"\x1b[S");
304 }
305 Ok(())
306 }
307
308 fn size(&self) -> io::Result<Size> {
309 Ok(self.size)
310 }
311
312 fn window_size(&mut self) -> io::Result<WindowSize> {
313 Ok(WindowSize {
315 columns_rows: self.size,
316 pixels: Size::new(self.size.width * 8, self.size.height * 16),
317 })
318 }
319
320 fn flush(&mut self) -> io::Result<()> {
321 Ok(())
323 }
324}
325
326pub fn terminal_setup_sequences() -> Vec<u8> {
331 use crate::services::terminal_modes::sequences as seq;
332
333 let mut buf = Vec::new();
334
335 buf.extend_from_slice(seq::ENTER_ALTERNATE_SCREEN);
337 buf.extend_from_slice(seq::ENABLE_MOUSE_CLICK);
339 buf.extend_from_slice(seq::ENABLE_MOUSE_DRAG);
340 buf.extend_from_slice(seq::ENABLE_MOUSE_MOTION);
341 buf.extend_from_slice(seq::ENABLE_SGR_MOUSE);
342 buf.extend_from_slice(seq::ENABLE_FOCUS_EVENTS);
344 buf.extend_from_slice(seq::ENABLE_BRACKETED_PASTE);
346 buf.extend_from_slice(seq::HIDE_CURSOR);
348
349 buf
350}
351
352pub fn terminal_teardown_sequences() -> Vec<u8> {
357 use crate::services::terminal_modes::sequences as seq;
358
359 let mut buf = Vec::new();
360
361 buf.extend_from_slice(seq::SHOW_CURSOR);
363 buf.extend_from_slice(seq::RESET_CURSOR_STYLE);
365 buf.extend_from_slice(seq::DISABLE_BRACKETED_PASTE);
367 buf.extend_from_slice(seq::DISABLE_FOCUS_EVENTS);
369 buf.extend_from_slice(seq::DISABLE_SGR_MOUSE);
371 buf.extend_from_slice(seq::DISABLE_MOUSE_MOTION);
372 buf.extend_from_slice(seq::DISABLE_MOUSE_DRAG);
373 buf.extend_from_slice(seq::DISABLE_MOUSE_CLICK);
374 buf.extend_from_slice(seq::RESET_ATTRIBUTES);
376 buf.extend_from_slice(seq::LEAVE_ALTERNATE_SCREEN);
378
379 buf
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use ratatui::buffer::Buffer;
386 use ratatui::style::Style;
387
388 #[test]
389 fn test_size_tracks_dimensions() {
390 let mut backend = CaptureBackend::new(80, 24);
391 assert_eq!(backend.size().unwrap(), Size::new(80, 24));
392
393 backend.resize(120, 40);
394 assert_eq!(backend.size().unwrap(), Size::new(120, 40));
395 }
396
397 #[test]
398 fn test_clear_emits_ansi_clear_sequence() {
399 let mut backend = CaptureBackend::new(80, 24);
400 backend.clear().unwrap();
401
402 let output = backend.take_buffer();
403 assert!(output.starts_with(b"\x1b[2J"));
405 }
406
407 #[test]
408 fn test_draw_outputs_cell_content() {
409 let mut backend = CaptureBackend::new(80, 24);
410
411 let mut buffer = Buffer::empty(ratatui::layout::Rect::new(0, 0, 5, 1));
412 buffer.set_string(0, 0, "Hello", Style::default());
413
414 let area = buffer.area;
415 backend
416 .draw(buffer.content.iter().enumerate().map(|(i, cell)| {
417 let x = (i as u16) % area.width;
418 let y = (i as u16) / area.width;
419 (x + area.x, y + area.y, cell)
420 }))
421 .unwrap();
422
423 let buf = backend.take_buffer();
424 let output = String::from_utf8_lossy(&buf);
425 assert!(output.contains("Hello"));
426 }
427
428 #[test]
429 fn test_cursor_visibility_emits_correct_sequences() {
430 let mut backend = CaptureBackend::new(80, 24);
431
432 backend.hide_cursor().unwrap();
433 let output = backend.take_buffer();
434 assert_eq!(output, b"\x1b[?25l"); backend.show_cursor().unwrap();
437 let output = backend.take_buffer();
438 assert_eq!(output, b"\x1b[?25h"); }
440
441 #[test]
442 fn test_cursor_visibility_always_emits_hide() {
443 let mut backend = CaptureBackend::new(80, 24);
444
445 backend.hide_cursor().unwrap();
446 backend.clear_buffer();
447
448 backend.hide_cursor().unwrap();
450 assert_eq!(backend.take_buffer(), b"\x1b[?25l");
451 }
452
453 #[test]
454 fn test_take_buffer_clears_internal_buffer() {
455 let mut backend = CaptureBackend::new(80, 24);
456 backend.clear().unwrap();
457
458 let first = backend.take_buffer();
459 assert!(!first.is_empty());
460
461 let second = backend.take_buffer();
462 assert!(second.is_empty());
463 }
464
465 #[test]
466 fn test_setup_sequences_enable_features() {
467 let setup = terminal_setup_sequences();
468 let setup_str = String::from_utf8_lossy(&setup);
469
470 assert!(setup_str.contains("\x1b[?1049h"));
472 assert!(setup_str.contains("\x1b[?1000h"));
474 assert!(setup_str.contains("\x1b[?1004h"));
476 }
477
478 #[test]
479 fn test_teardown_sequences_disable_features() {
480 let teardown = terminal_teardown_sequences();
481 let teardown_str = String::from_utf8_lossy(&teardown);
482
483 assert!(teardown_str.contains("\x1b[?1049l"));
485 assert!(teardown_str.contains("\x1b[0m"));
487 }
488
489 #[test]
490 fn test_clear_region_variants() {
491 let mut backend = CaptureBackend::new(80, 24);
492
493 backend.clear_region(ClearType::AfterCursor).unwrap();
494 assert!(backend.take_buffer().ends_with(b"\x1b[J"));
495
496 backend.clear_region(ClearType::CurrentLine).unwrap();
497 assert!(backend.take_buffer().ends_with(b"\x1b[2K"));
498 }
499}