1use alacritty_terminal::event::{Event, EventListener};
31use alacritty_terminal::grid::Scroll;
32use alacritty_terminal::index::{Column, Line};
33use alacritty_terminal::term::test::TermSize;
34use alacritty_terminal::term::{Config as TermConfig, Term, TermMode};
35use alacritty_terminal::vte::ansi::Processor;
36use std::io::{self, Write};
37use std::sync::{Arc, Mutex};
38
39const SCROLLBACK_LINES: usize = 200_000;
41
42#[derive(Clone)]
48struct PtyWriteListener {
49 write_queue: Arc<Mutex<Vec<String>>>,
51}
52
53impl PtyWriteListener {
54 fn new() -> Self {
55 Self {
56 write_queue: Arc::new(Mutex::new(Vec::new())),
57 }
58 }
59}
60
61impl EventListener for PtyWriteListener {
62 fn send_event(&self, event: Event) {
63 if let Event::PtyWrite(text) = event {
64 if let Ok(mut queue) = self.write_queue.lock() {
65 queue.push(text);
66 }
67 }
68 }
70}
71
72pub struct TerminalState {
74 term: Term<PtyWriteListener>,
76 parser: Processor,
78 cols: u16,
80 rows: u16,
81 dirty: bool,
83 terminal_title: String,
85 synced_history_lines: usize,
87 backing_file_history_end: u64,
89 pty_write_queue: Arc<Mutex<Vec<String>>>,
91}
92
93impl TerminalState {
94 pub fn new(cols: u16, rows: u16) -> Self {
96 let size = TermSize::new(cols as usize, rows as usize);
97 let config = TermConfig {
98 scrolling_history: SCROLLBACK_LINES,
99 ..Default::default()
100 };
101 let listener = PtyWriteListener::new();
102 let pty_write_queue = listener.write_queue.clone();
103 let term = Term::new(config, &size, listener);
104
105 Self {
106 term,
107 parser: Processor::new(),
108 cols,
109 rows,
110 dirty: true,
111 terminal_title: String::new(),
112 synced_history_lines: 0,
113 backing_file_history_end: 0,
114 pty_write_queue,
115 }
116 }
117
118 pub fn drain_pty_write_queue(&self) -> Vec<String> {
123 if let Ok(mut queue) = self.pty_write_queue.lock() {
124 std::mem::take(&mut *queue)
125 } else {
126 Vec::new()
127 }
128 }
129
130 pub fn process_output(&mut self, data: &[u8]) {
132 self.parser.advance(&mut self.term, data);
133 self.dirty = true;
134 }
135
136 pub fn resize(&mut self, cols: u16, rows: u16) {
138 if cols != self.cols || rows != self.rows {
139 self.cols = cols;
140 self.rows = rows;
141 let size = TermSize::new(cols as usize, rows as usize);
142 self.term.resize(size);
143 self.dirty = true;
144 }
145 }
146
147 pub fn size(&self) -> (u16, u16) {
149 (self.cols, self.rows)
150 }
151
152 pub fn is_dirty(&self) -> bool {
154 self.dirty
155 }
156
157 pub fn mark_clean(&mut self) {
159 self.dirty = false;
160 }
161
162 pub fn cursor_position(&self) -> (u16, u16) {
164 let cursor = self.term.grid().cursor.point;
165 (cursor.column.0 as u16, cursor.line.0 as u16)
166 }
167
168 pub fn cursor_visible(&self) -> bool {
170 true
173 }
174
175 pub fn get_line(&self, row: u16) -> Vec<TerminalCell> {
181 use alacritty_terminal::index::{Column, Line};
182 use alacritty_terminal::term::cell::Flags;
183
184 let grid = self.term.grid();
185 let display_offset = grid.display_offset();
186
187 let line = Line(row as i32 - display_offset as i32);
190
191 if row >= self.rows {
193 return vec![TerminalCell::default(); self.cols as usize];
194 }
195
196 let row_data = &grid[line];
197 let mut cells = Vec::with_capacity(self.cols as usize);
198
199 for col in 0..self.cols as usize {
200 let cell = &row_data[Column(col)];
201 let c = cell.c;
202
203 let fg = color_to_rgb(&cell.fg);
205 let bg = color_to_rgb(&cell.bg);
206
207 let flags = cell.flags;
209 let bold = flags.contains(Flags::BOLD);
210 let italic = flags.contains(Flags::ITALIC);
211 let underline = flags.contains(Flags::UNDERLINE);
212 let inverse = flags.contains(Flags::INVERSE);
213
214 cells.push(TerminalCell {
215 c,
216 fg,
217 bg,
218 bold,
219 italic,
220 underline,
221 inverse,
222 });
223 }
224
225 cells
226 }
227
228 pub fn content_string(&self) -> String {
230 let mut result = String::new();
231 for row in 0..self.rows {
232 let line = self.get_line(row);
233 for cell in line {
234 result.push(cell.c);
235 }
236 result.push('\n');
237 }
238 result
239 }
240
241 #[allow(dead_code)]
249 pub fn full_content_string(&self) -> String {
250 use alacritty_terminal::grid::Dimensions;
251 use alacritty_terminal::index::{Column, Line};
252
253 let grid = self.term.grid();
254 let history_size = grid.history_size();
255 let mut result = String::new();
256
257 for i in (1..=history_size).rev() {
260 let line = Line(-(i as i32));
261 let row_data = &grid[line];
262 let mut line_str = String::new();
263 for col in 0..self.cols as usize {
264 line_str.push(row_data[Column(col)].c);
265 }
266 let trimmed = line_str.trim_end();
267 result.push_str(trimmed);
268 result.push('\n');
269 }
270
271 for row in 0..self.rows {
273 let line = self.get_line(row);
274 let line_str: String = line.iter().map(|c| c.c).collect();
275 let trimmed = line_str.trim_end();
276 result.push_str(trimmed);
277 if row < self.rows - 1 {
278 result.push('\n');
279 }
280 }
281
282 result
283 }
284
285 pub fn history_size(&self) -> usize {
287 use alacritty_terminal::grid::Dimensions;
288 self.term.grid().history_size()
289 }
290
291 pub fn title(&self) -> &str {
293 &self.terminal_title
294 }
295
296 pub fn set_title(&mut self, title: String) {
298 self.terminal_title = title;
299 }
300
301 pub fn scroll_to_bottom(&mut self) {
304 self.term.scroll_display(Scroll::Bottom);
305 self.dirty = true;
306 }
307
308 pub fn is_alternate_screen(&self) -> bool {
315 self.term.mode().contains(TermMode::ALT_SCREEN)
316 }
317
318 pub fn wants_mouse_events(&self) -> bool {
321 let mode = self.term.mode();
322 mode.intersects(
323 TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG,
324 )
325 }
326
327 pub fn uses_sgr_mouse(&self) -> bool {
329 self.term.mode().contains(TermMode::SGR_MOUSE)
330 }
331
332 pub fn uses_alternate_scroll(&self) -> bool {
335 self.term.mode().contains(TermMode::ALTERNATE_SCROLL)
336 }
337
338 pub fn is_app_cursor(&self) -> bool {
342 self.term.mode().contains(TermMode::APP_CURSOR)
343 }
344
345 pub fn flush_new_scrollback<W: Write>(&mut self, writer: &mut W) -> io::Result<usize> {
358 use alacritty_terminal::grid::Dimensions;
359
360 let grid = self.term.grid();
361 let current_history = grid.history_size();
362
363 if current_history <= self.synced_history_lines {
364 return Ok(0);
365 }
366
367 let new_count = current_history - self.synced_history_lines;
368
369 for i in 0..new_count {
378 let line_idx = -((new_count - i) as i32);
382 self.write_grid_line(writer, Line(line_idx))?;
383 }
384
385 self.synced_history_lines = current_history;
386 Ok(new_count)
391 }
392
393 pub fn append_visible_screen<W: Write>(&self, writer: &mut W) -> io::Result<()> {
402 let grid = self.term.grid();
403
404 let mut last_non_empty_row: i32 = -1;
406 for row in 0..self.rows as i32 {
407 let row_data = &grid[Line(row)];
408 let is_empty = (0..self.cols as usize)
409 .all(|col| row_data[Column(col)].c == ' ' || row_data[Column(col)].c == '\0');
410 if !is_empty {
411 last_non_empty_row = row;
412 }
413 }
414
415 for row in 0..=last_non_empty_row {
417 self.write_grid_line(writer, Line(row))?;
418 }
419 Ok(())
420 }
421
422 fn write_grid_line<W: Write>(&self, writer: &mut W, line: Line) -> io::Result<()> {
428 use alacritty_terminal::term::cell::Flags;
429
430 let grid = self.term.grid();
431 let row_data = &grid[line];
432
433 let mut line_str = String::with_capacity(self.cols as usize * 2);
434 let mut current_fg: Option<(u8, u8, u8)> = None;
435 let mut current_bg: Option<(u8, u8, u8)> = None;
436 let mut current_bold = false;
437 let mut current_italic = false;
438 let mut current_underline = false;
439
440 for col in 0..self.cols as usize {
441 let cell = &row_data[Column(col)];
442 let fg = color_to_rgb(&cell.fg);
443 let bg = color_to_rgb(&cell.bg);
444 let flags = cell.flags;
445 let bold = flags.contains(Flags::BOLD);
446 let italic = flags.contains(Flags::ITALIC);
447 let underline = flags.contains(Flags::UNDERLINE);
448
449 let fg_changed = fg != current_fg;
451 let bg_changed = bg != current_bg;
452 let bold_changed = bold != current_bold;
453 let italic_changed = italic != current_italic;
454 let underline_changed = underline != current_underline;
455
456 if fg_changed || bg_changed || bold_changed || italic_changed || underline_changed {
457 let mut codes: Vec<String> = Vec::new();
459
460 if (current_bold && !bold)
462 || (current_italic && !italic)
463 || (current_underline && !underline)
464 {
465 codes.push("0".to_string());
466 if bold {
468 codes.push("1".to_string());
469 }
470 if italic {
471 codes.push("3".to_string());
472 }
473 if underline {
474 codes.push("4".to_string());
475 }
476 if let Some((r, g, b)) = fg {
477 codes.push(format!("38;2;{};{};{}", r, g, b));
478 }
479 if let Some((r, g, b)) = bg {
480 codes.push(format!("48;2;{};{};{}", r, g, b));
481 }
482 } else {
483 if bold_changed && bold {
485 codes.push("1".to_string());
486 }
487 if italic_changed && italic {
488 codes.push("3".to_string());
489 }
490 if underline_changed && underline {
491 codes.push("4".to_string());
492 }
493 if fg_changed {
494 if let Some((r, g, b)) = fg {
495 codes.push(format!("38;2;{};{};{}", r, g, b));
496 } else {
497 codes.push("39".to_string()); }
499 }
500 if bg_changed {
501 if let Some((r, g, b)) = bg {
502 codes.push(format!("48;2;{};{};{}", r, g, b));
503 } else {
504 codes.push("49".to_string()); }
506 }
507 }
508
509 if !codes.is_empty() {
510 line_str.push_str(&format!("\x1b[{}m", codes.join(";")));
511 }
512
513 current_fg = fg;
514 current_bg = bg;
515 current_bold = bold;
516 current_italic = italic;
517 current_underline = underline;
518 }
519
520 line_str.push(cell.c);
521 }
522
523 if current_fg.is_some()
525 || current_bg.is_some()
526 || current_bold
527 || current_italic
528 || current_underline
529 {
530 line_str.push_str("\x1b[0m");
531 }
532
533 let trimmed = line_str.trim_end_matches([' ', '\0']);
535 writeln!(writer, "{}", trimmed)
536 }
537
538 pub fn backing_file_history_end(&self) -> u64 {
543 self.backing_file_history_end
544 }
545
546 pub fn set_backing_file_history_end(&mut self, offset: u64) {
550 self.backing_file_history_end = offset;
551 }
552
553 pub fn synced_history_lines(&self) -> usize {
555 self.synced_history_lines
556 }
557
558 pub fn reset_sync_state(&mut self) {
560 self.synced_history_lines = 0;
561 self.backing_file_history_end = 0;
562 }
563}
564
565#[derive(Debug, Clone)]
567pub struct TerminalCell {
568 pub c: char,
570 pub fg: Option<(u8, u8, u8)>,
572 pub bg: Option<(u8, u8, u8)>,
574 pub bold: bool,
576 pub italic: bool,
578 pub underline: bool,
580 pub inverse: bool,
582}
583
584impl Default for TerminalCell {
585 fn default() -> Self {
586 Self {
587 c: ' ',
588 fg: None,
589 bg: None,
590 bold: false,
591 italic: false,
592 underline: false,
593 inverse: false,
594 }
595 }
596}
597
598fn color_to_rgb(color: &alacritty_terminal::vte::ansi::Color) -> Option<(u8, u8, u8)> {
600 use alacritty_terminal::vte::ansi::Color;
601
602 match color {
603 Color::Spec(rgb) => Some((rgb.r, rgb.g, rgb.b)),
604 Color::Named(named) => {
605 let rgb = match named {
608 alacritty_terminal::vte::ansi::NamedColor::Black => (0, 0, 0),
609 alacritty_terminal::vte::ansi::NamedColor::Red => (205, 49, 49),
610 alacritty_terminal::vte::ansi::NamedColor::Green => (13, 188, 121),
611 alacritty_terminal::vte::ansi::NamedColor::Yellow => (229, 229, 16),
612 alacritty_terminal::vte::ansi::NamedColor::Blue => (36, 114, 200),
613 alacritty_terminal::vte::ansi::NamedColor::Magenta => (188, 63, 188),
614 alacritty_terminal::vte::ansi::NamedColor::Cyan => (17, 168, 205),
615 alacritty_terminal::vte::ansi::NamedColor::White => (229, 229, 229),
616 alacritty_terminal::vte::ansi::NamedColor::BrightBlack => (102, 102, 102),
617 alacritty_terminal::vte::ansi::NamedColor::BrightRed => (241, 76, 76),
618 alacritty_terminal::vte::ansi::NamedColor::BrightGreen => (35, 209, 139),
619 alacritty_terminal::vte::ansi::NamedColor::BrightYellow => (245, 245, 67),
620 alacritty_terminal::vte::ansi::NamedColor::BrightBlue => (59, 142, 234),
621 alacritty_terminal::vte::ansi::NamedColor::BrightMagenta => (214, 112, 214),
622 alacritty_terminal::vte::ansi::NamedColor::BrightCyan => (41, 184, 219),
623 alacritty_terminal::vte::ansi::NamedColor::BrightWhite => (255, 255, 255),
624 alacritty_terminal::vte::ansi::NamedColor::Foreground => return None,
625 alacritty_terminal::vte::ansi::NamedColor::Background => return None,
626 alacritty_terminal::vte::ansi::NamedColor::Cursor => return None,
627 _ => return None,
628 };
629 Some(rgb)
630 }
631 Color::Indexed(idx) => {
632 let idx = *idx as usize;
635 if idx < 16 {
636 let colors = [
638 (0, 0, 0), (205, 49, 49), (13, 188, 121), (229, 229, 16), (36, 114, 200), (188, 63, 188), (17, 168, 205), (229, 229, 229), (102, 102, 102), (241, 76, 76), (35, 209, 139), (245, 245, 67), (59, 142, 234), (214, 112, 214), (41, 184, 219), (255, 255, 255), ];
655 Some(colors[idx])
656 } else if idx < 232 {
657 let idx = idx - 16;
659 let r = (idx / 36) * 51;
660 let g = ((idx / 6) % 6) * 51;
661 let b = (idx % 6) * 51;
662 Some((r as u8, g as u8, b as u8))
663 } else {
664 let gray = (idx - 232) * 10 + 8;
666 Some((gray as u8, gray as u8, gray as u8))
667 }
668 }
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 #[test]
677 fn test_terminal_state_new() {
678 let state = TerminalState::new(80, 24);
679 assert_eq!(state.size(), (80, 24));
680 assert!(state.is_dirty());
681 }
682
683 #[test]
684 fn test_terminal_process_output() {
685 let mut state = TerminalState::new(80, 24);
686 state.process_output(b"Hello, World!");
687 let content = state.content_string();
688 assert!(content.contains("Hello, World!"));
689 }
690
691 #[test]
692 fn test_terminal_resize() {
693 let mut state = TerminalState::new(80, 24);
694 state.mark_clean();
695 assert!(!state.is_dirty());
696
697 state.resize(100, 30);
698 assert_eq!(state.size(), (100, 30));
699 assert!(state.is_dirty());
700 }
701
702 #[test]
703 fn test_flush_new_scrollback_no_history() {
704 let mut state = TerminalState::new(80, 24);
706 state.process_output(b"Hello");
707
708 let mut buffer = Vec::new();
709 let count = state.flush_new_scrollback(&mut buffer).unwrap();
710
711 assert_eq!(count, 0, "No scrollback yet, should flush 0 lines");
712 assert!(buffer.is_empty(), "Buffer should be empty");
713 }
714
715 #[test]
716 fn test_flush_new_scrollback_after_scroll() {
717 let mut state = TerminalState::new(80, 10); for i in 1..=20 {
722 state.process_output(format!("Line {}\r\n", i).as_bytes());
723 }
724
725 let mut buffer = Vec::new();
726 let count = state.flush_new_scrollback(&mut buffer).unwrap();
727
728 let output = String::from_utf8_lossy(&buffer);
730 eprintln!(
731 "Scrollback test: count={}, synced={}, buffer_len={}, output:\n{}",
732 count,
733 state.synced_history_lines(),
734 buffer.len(),
735 output
736 );
737
738 assert!(count > 0, "Should have some scrollback lines");
740 assert!(
741 output.contains("Line 1"),
742 "Scrollback should contain Line 1"
743 );
744 }
745
746 #[test]
747 fn test_append_visible_screen() {
748 let mut state = TerminalState::new(80, 5);
749 state.process_output(b"Line A\r\nLine B\r\nLine C\r\n");
750
751 let mut buffer = Vec::new();
752 state.append_visible_screen(&mut buffer).unwrap();
753
754 let output = String::from_utf8_lossy(&buffer);
755 assert!(
756 output.contains("Line A"),
757 "Visible screen should contain Line A"
758 );
759 assert!(
760 output.contains("Line B"),
761 "Visible screen should contain Line B"
762 );
763 assert!(
764 output.contains("Line C"),
765 "Visible screen should contain Line C"
766 );
767 }
768
769 #[test]
770 fn test_scrollback_then_visible_no_duplication() {
771 let mut state = TerminalState::new(80, 5); for i in 1..=15 {
777 state.process_output(format!("UNIQUELINE_{:02}\r\n", i).as_bytes());
778 }
779
780 let mut scrollback_buffer = Vec::new();
782 let scrollback_count = state.flush_new_scrollback(&mut scrollback_buffer).unwrap();
783 let scrollback_output = String::from_utf8_lossy(&scrollback_buffer);
784
785 let mut visible_buffer = Vec::new();
787 state.append_visible_screen(&mut visible_buffer).unwrap();
788 let visible_output = String::from_utf8_lossy(&visible_buffer);
789
790 eprintln!(
791 "Scrollback ({} lines):\n{}",
792 scrollback_count, scrollback_output
793 );
794 eprintln!("Visible screen:\n{}", visible_output);
795
796 let combined = format!("{}{}", scrollback_output, visible_output);
798
799 for i in 1..=15 {
801 let pattern = format!("UNIQUELINE_{:02}", i);
802 let count = combined.matches(&pattern).count();
803 assert!(
804 count >= 1,
805 "Line {} should appear at least once, but found {} times",
806 i,
807 count
808 );
809 assert!(
811 count <= 2,
812 "Line {} appears {} times - too much duplication",
813 i,
814 count
815 );
816 }
817 }
818
819 #[test]
820 fn test_backing_file_history_end_tracking() {
821 let mut state = TerminalState::new(80, 5);
822
823 assert_eq!(state.backing_file_history_end(), 0);
825
826 state.set_backing_file_history_end(1234);
828 assert_eq!(state.backing_file_history_end(), 1234);
829
830 state.reset_sync_state();
832 assert_eq!(state.backing_file_history_end(), 0);
833 assert_eq!(state.synced_history_lines(), 0);
834 }
835
836 #[test]
837 fn test_multiple_flush_cycles_no_duplication() {
838 use alacritty_terminal::grid::Dimensions;
839
840 let mut state = TerminalState::new(80, 5);
842
843 for i in 1..=10 {
846 state.process_output(format!("Batch1-Line{}\r\n", i).as_bytes());
847 }
848
849 let history1 = state.term.grid().history_size();
850 eprintln!("After Batch1: history_size={}", history1);
851 assert_eq!(
852 history1, 6,
853 "After 10 lines in 5-row terminal, 6 should be in history"
854 );
855
856 let mut buffer1 = Vec::new();
858 let count1 = state.flush_new_scrollback(&mut buffer1).unwrap();
859 let output1 = String::from_utf8_lossy(&buffer1);
860 eprintln!("First flush: {} lines\n{}", count1, output1);
861
862 assert_eq!(count1, 6);
863 assert!(output1.contains("Batch1-Line1"));
864 assert!(output1.contains("Batch1-Line6"));
865 assert!(
866 !output1.contains("Batch1-Line7"),
867 "Line 7 should still be visible, not in scrollback"
868 );
869
870 let mut buffer2 = Vec::new();
872 let count2 = state.flush_new_scrollback(&mut buffer2).unwrap();
873 assert_eq!(count2, 0, "Second flush without new output should be 0");
874
875 for i in 1..=10 {
878 state.process_output(format!("Batch2-Line{}\r\n", i).as_bytes());
879 }
880
881 let history3 = state.term.grid().history_size();
882 eprintln!("After Batch2: history_size={}", history3);
883
884 let mut buffer3 = Vec::new();
887 let count3 = state.flush_new_scrollback(&mut buffer3).unwrap();
888 let output3 = String::from_utf8_lossy(&buffer3);
889 eprintln!("Third flush: {} lines\n{}", count3, output3);
890
891 assert_eq!(count3, 10, "Should flush 10 new lines");
892 assert!(
894 output3.contains("Batch1-Line7"),
895 "Batch1-Line7 should be in third flush (was visible, now scrolled)"
896 );
897 assert!(output3.contains("Batch1-Line10"));
898 assert!(output3.contains("Batch2-Line1"));
900 assert!(output3.contains("Batch2-Line6"));
901 assert!(
903 !output3.contains("Batch1-Line1\n"),
904 "Batch1-Line1 was already flushed, shouldn't appear again"
905 );
906 assert!(
907 !output3.contains("Batch1-Line6\n"),
908 "Batch1-Line6 was already flushed, shouldn't appear again"
909 );
910 }
911
912 #[test]
913 fn test_dsr_cursor_position_response() {
914 let mut state = TerminalState::new(80, 24);
917
918 assert!(
920 state.drain_pty_write_queue().is_empty(),
921 "Write queue should be empty initially"
922 );
923
924 state.process_output(b"\x1b[6n");
926
927 let responses = state.drain_pty_write_queue();
929 assert_eq!(responses.len(), 1, "Should have exactly one response");
930
931 let response = &responses[0];
932 assert!(
935 response.starts_with("\x1b["),
936 "Response should start with ESC["
937 );
938 assert!(response.ends_with("R"), "Response should end with R");
939 eprintln!("DSR response: {:?}", response);
940
941 assert!(
943 state.drain_pty_write_queue().is_empty(),
944 "Write queue should be empty after draining"
945 );
946 }
947
948 #[test]
949 fn test_dsr_response_after_cursor_move() {
950 let mut state = TerminalState::new(80, 24);
952
953 state.process_output(b"\x1b[5;10H");
956
957 state.process_output(b"\x1b[6n");
959
960 let responses = state.drain_pty_write_queue();
961 assert_eq!(responses.len(), 1);
962
963 let response = &responses[0];
964 assert_eq!(response, "\x1b[5;10R", "Response should be \\x1b[5;10R");
966 }
967}