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(mouse_hover_enabled: bool) -> Vec<u8> {
336 use crate::services::terminal_modes::sequences as seq;
337
338 let mut buf = Vec::new();
339
340 buf.extend_from_slice(seq::ENTER_ALTERNATE_SCREEN);
342 buf.extend_from_slice(seq::ENABLE_MOUSE_CLICK);
344 buf.extend_from_slice(seq::ENABLE_MOUSE_DRAG);
345 if cfg!(windows) {
349 if mouse_hover_enabled {
350 buf.extend_from_slice(seq::ENABLE_MOUSE_MOTION);
351 }
352 } else {
353 buf.extend_from_slice(seq::ENABLE_MOUSE_MOTION);
354 }
355 buf.extend_from_slice(seq::ENABLE_SGR_MOUSE);
356 buf.extend_from_slice(seq::ENABLE_FOCUS_EVENTS);
358 buf.extend_from_slice(seq::ENABLE_BRACKETED_PASTE);
360 buf.extend_from_slice(seq::HIDE_CURSOR);
362
363 buf
364}
365
366pub fn terminal_teardown_sequences() -> Vec<u8> {
371 use crate::services::terminal_modes::sequences as seq;
372
373 let mut buf = Vec::new();
374
375 buf.extend_from_slice(seq::SHOW_CURSOR);
377 buf.extend_from_slice(seq::RESET_CURSOR_STYLE);
379 buf.extend_from_slice(seq::DISABLE_BRACKETED_PASTE);
381 buf.extend_from_slice(seq::DISABLE_FOCUS_EVENTS);
383 buf.extend_from_slice(seq::DISABLE_SGR_MOUSE);
385 buf.extend_from_slice(seq::DISABLE_MOUSE_MOTION);
386 buf.extend_from_slice(seq::DISABLE_MOUSE_DRAG);
387 buf.extend_from_slice(seq::DISABLE_MOUSE_CLICK);
388 buf.extend_from_slice(seq::RESET_ATTRIBUTES);
390 buf.extend_from_slice(seq::LEAVE_ALTERNATE_SCREEN);
392
393 buf
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use ratatui::buffer::Buffer;
400 use ratatui::style::Style;
401
402 #[test]
403 fn test_size_tracks_dimensions() {
404 let mut backend = CaptureBackend::new(80, 24);
405 assert_eq!(backend.size().unwrap(), Size::new(80, 24));
406
407 backend.resize(120, 40);
408 assert_eq!(backend.size().unwrap(), Size::new(120, 40));
409 }
410
411 #[test]
412 fn test_clear_emits_ansi_clear_sequence() {
413 let mut backend = CaptureBackend::new(80, 24);
414 backend.clear().unwrap();
415
416 let output = backend.take_buffer();
417 assert!(output.starts_with(b"\x1b[2J"));
419 }
420
421 #[test]
422 fn test_draw_outputs_cell_content() {
423 let mut backend = CaptureBackend::new(80, 24);
424
425 let mut buffer = Buffer::empty(ratatui::layout::Rect::new(0, 0, 5, 1));
426 buffer.set_string(0, 0, "Hello", Style::default());
427
428 let area = buffer.area;
429 backend
430 .draw(buffer.content.iter().enumerate().map(|(i, cell)| {
431 let x = (i as u16) % area.width;
432 let y = (i as u16) / area.width;
433 (x + area.x, y + area.y, cell)
434 }))
435 .unwrap();
436
437 let buf = backend.take_buffer();
438 let output = String::from_utf8_lossy(&buf);
439 assert!(output.contains("Hello"));
440 }
441
442 #[test]
443 fn test_cursor_visibility_emits_correct_sequences() {
444 let mut backend = CaptureBackend::new(80, 24);
445
446 backend.hide_cursor().unwrap();
447 let output = backend.take_buffer();
448 assert_eq!(output, b"\x1b[?25l"); backend.show_cursor().unwrap();
451 let output = backend.take_buffer();
452 assert_eq!(output, b"\x1b[?25h"); }
454
455 #[test]
456 fn test_cursor_visibility_always_emits_hide() {
457 let mut backend = CaptureBackend::new(80, 24);
458
459 backend.hide_cursor().unwrap();
460 backend.clear_buffer();
461
462 backend.hide_cursor().unwrap();
464 assert_eq!(backend.take_buffer(), b"\x1b[?25l");
465 }
466
467 #[test]
468 fn test_take_buffer_clears_internal_buffer() {
469 let mut backend = CaptureBackend::new(80, 24);
470 backend.clear().unwrap();
471
472 let first = backend.take_buffer();
473 assert!(!first.is_empty());
474
475 let second = backend.take_buffer();
476 assert!(second.is_empty());
477 }
478
479 #[test]
480 fn test_setup_sequences_enable_features() {
481 let setup = terminal_setup_sequences(true);
482 let setup_str = String::from_utf8_lossy(&setup);
483
484 assert!(setup_str.contains("\x1b[?1049h"));
486 assert!(setup_str.contains("\x1b[?1000h"));
488 assert!(setup_str.contains("\x1b[?1004h"));
490 }
491
492 #[test]
493 fn test_teardown_sequences_disable_features() {
494 let teardown = terminal_teardown_sequences();
495 let teardown_str = String::from_utf8_lossy(&teardown);
496
497 assert!(teardown_str.contains("\x1b[?1049l"));
499 assert!(teardown_str.contains("\x1b[0m"));
501 }
502
503 #[test]
504 fn test_clear_region_variants() {
505 let mut backend = CaptureBackend::new(80, 24);
506
507 backend.clear_region(ClearType::AfterCursor).unwrap();
508 assert!(backend.take_buffer().ends_with(b"\x1b[J"));
509
510 backend.clear_region(ClearType::CurrentLine).unwrap();
511 assert!(backend.take_buffer().ends_with(b"\x1b[2K"));
512 }
513}