1use regex::Regex;
2use std::collections::{HashMap, VecDeque};
3use std::sync::{Arc, Mutex, RwLock};
4use std::time::Instant;
5use tracing::{debug, warn};
6use vte::{Parser, Perform};
7
8#[allow(unused_imports)]
10use crate::state::ansi_codes;
11
12pub const MAX_SCREEN_LINES: usize = 10000;
14pub const DEFAULT_MAX_SCREEN_LINES: usize = 500;
16const DEFAULT_COLUMNS: usize = 200;
21pub const MAX_OUTPUT_SIZE: usize = 500_000;
23const CACHE_TTL: u64 = 300; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub struct CellStyle(u32);
29
30impl CellStyle {
31 pub const BOLD: Self = Self(1 << 0);
32 pub const UNDERLINE: Self = Self(1 << 1);
33 pub const BLINK: Self = Self(1 << 2);
34 pub const REVERSE: Self = Self(1 << 3);
35 pub const ITALIC: Self = Self(1 << 4);
36 pub const STRIKETHROUGH: Self = Self(1 << 5);
37 pub const DIM: Self = Self(1 << 6);
38 pub const DOUBLE_UNDERLINE: Self = Self(1 << 7);
39 pub const FRAMED: Self = Self(1 << 8);
40 pub const ENCIRCLED: Self = Self(1 << 9);
41 pub const OVERLINED: Self = Self(1 << 10);
42 pub const FRAKTUR: Self = Self(1 << 11);
43 pub const CONCEAL: Self = Self(1 << 12);
44 pub const SUPERSCRIPT: Self = Self(1 << 13);
45 pub const SUBSCRIPT: Self = Self(1 << 14);
46 pub const HYPERLINK: Self = Self(1 << 15);
47
48 #[must_use]
49 pub const fn union(self, other: Self) -> Self {
50 Self(self.0 | other.0)
51 }
52
53 #[must_use]
54 pub const fn contains(self, flag: Self) -> bool {
55 self.0 & flag.0 != 0
56 }
57
58 pub fn set(&mut self, flag: Self, enabled: bool) {
59 if enabled {
60 self.0 |= flag.0;
61 } else {
62 self.0 &= !flag.0;
63 }
64 }
65}
66
67#[derive(Debug, Clone, Default, PartialEq, Eq)]
69pub struct ScreenCellAttributes {
70 pub style: CellStyle,
72 pub fg_color: Option<TerminalColor>,
74 pub bg_color: Option<TerminalColor>,
76 pub hyperlink_url: Option<String>,
78 pub font: u8,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ScreenCell {
85 pub character: char,
87 pub style: CellStyle,
89 pub fg_color: Option<TerminalColor>,
91 pub bg_color: Option<TerminalColor>,
93 pub hyperlink_url: Option<String>,
95 pub font: u8,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum TerminalColor {
102 Basic(u8),
104 Color256(u8),
106 TrueColor { r: u8, g: u8, b: u8 },
108 Named(String),
110}
111
112impl ScreenCell {
113 fn new(character: char, attributes: ScreenCellAttributes) -> Self {
114 Self {
115 character,
116 style: attributes.style,
117 fg_color: attributes.fg_color,
118 bg_color: attributes.bg_color,
119 hyperlink_url: attributes.hyperlink_url,
120 font: attributes.font,
121 }
122 }
123}
124
125impl Default for ScreenCell {
126 fn default() -> Self {
127 Self {
128 character: ' ',
129 style: CellStyle::default(),
130 fg_color: None,
131 bg_color: None,
132 hyperlink_url: None,
133 font: 0, }
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct Screen {
141 pub lines: VecDeque<Vec<ScreenCell>>,
143 pub cursor_position: (usize, usize),
145 pub columns: usize,
147 pub cursor_visible: bool,
149 pub max_lines: usize,
151 last_modified: Instant,
153}
154
155impl Default for Screen {
156 fn default() -> Self {
157 let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
158 lines.push_back(vec![ScreenCell::default(); DEFAULT_COLUMNS]);
159
160 Self {
161 lines,
162 cursor_position: (0, 0),
163 columns: DEFAULT_COLUMNS,
164 cursor_visible: true,
165 max_lines: DEFAULT_MAX_SCREEN_LINES,
166 last_modified: Instant::now(),
167 }
168 }
169}
170
171impl Screen {
172 pub fn new(columns: usize) -> Self {
174 let columns = columns.max(1);
177 let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
178 lines.push_back(vec![ScreenCell::default(); columns]);
179
180 Self {
181 lines,
182 cursor_position: (0, 0),
183 columns,
184 cursor_visible: true,
185 max_lines: DEFAULT_MAX_SCREEN_LINES,
186 last_modified: Instant::now(),
187 }
188 }
189
190 pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
192 let columns = columns.max(1);
194 let mut lines = VecDeque::with_capacity(max_lines.min(MAX_SCREEN_LINES));
195 lines.push_back(vec![ScreenCell::default(); columns]);
196
197 Self {
198 lines,
199 cursor_position: (0, 0),
200 columns,
201 cursor_visible: true,
202 max_lines: max_lines.min(MAX_SCREEN_LINES),
203 last_modified: Instant::now(),
204 }
205 }
206
207 pub fn cursor_row(&self) -> usize {
209 self.cursor_position.0
210 }
211
212 pub fn cursor_col(&self) -> usize {
214 self.cursor_position.1
215 }
216
217 fn ensure_line(&mut self, line_idx: usize) {
219 while self.lines.len() <= line_idx {
221 self.lines.push_back(vec![ScreenCell::default(); self.columns]);
222 }
223
224 while self.lines.len() > self.max_lines {
226 self.lines.pop_front();
227
228 if self.cursor_position.0 > 0 {
230 self.cursor_position.0 -= 1;
231 }
232 }
233
234 self.last_modified = Instant::now();
235 }
236
237 fn ensure_cursor_position(&mut self) {
239 self.ensure_line(self.cursor_position.0);
240
241 if self.cursor_position.1 >= self.columns {
243 self.cursor_position.1 = self.columns - 1;
244 }
245
246 self.last_modified = Instant::now();
247 }
248
249 pub fn put_char(&mut self, c: char, attributes: ScreenCellAttributes) {
251 self.ensure_cursor_position();
252
253 let row = self.cursor_position.0;
255 let col = self.cursor_position.1;
256
257 if col < self.lines[row].len() {
259 self.lines[row][col] = ScreenCell::new(c, attributes);
260 } else {
261 while self.lines[row].len() <= col {
263 self.lines[row].push(ScreenCell::default());
264 }
265 self.lines[row][col] = ScreenCell::new(c, attributes);
266 }
267
268 self.cursor_position.1 += 1;
270 if self.cursor_position.1 >= self.columns {
271 self.cursor_position.1 = 0;
272 self.cursor_position.0 += 1;
273 self.ensure_cursor_position();
274 }
275
276 self.last_modified = Instant::now();
277 }
278
279 #[allow(clippy::too_many_arguments)]
281 pub fn put_char_basic(
282 &mut self,
283 c: char,
284 style: CellStyle,
285 fg_color: Option<TerminalColor>,
286 bg_color: Option<TerminalColor>,
287 ) {
288 let attributes = ScreenCellAttributes { style, fg_color, bg_color, ..Default::default() };
289
290 self.put_char(c, attributes);
291 }
292
293 pub fn move_cursor(&mut self, row: usize, col: usize) {
295 self.cursor_position = (row, col);
296 self.ensure_cursor_position();
297 self.last_modified = Instant::now();
298 }
299
300 pub fn linefeed(&mut self) {
302 self.cursor_position.0 += 1;
303 self.ensure_cursor_position();
304 self.last_modified = Instant::now();
305 }
306
307 pub fn carriage_return(&mut self) {
309 self.cursor_position.1 = 0;
310 self.last_modified = Instant::now();
311 }
312
313 pub fn clear(&mut self) {
315 self.lines.clear();
316 self.lines.push_back(vec![ScreenCell::default(); self.columns]);
317 self.cursor_position = (0, 0);
318 self.last_modified = Instant::now();
319 }
320
321 pub fn clear_line_forward(&mut self) {
323 let row = self.cursor_position.0;
324 let col = self.cursor_position.1;
325
326 if row < self.lines.len() {
327 for i in col..self.lines[row].len() {
328 self.lines[row][i] = ScreenCell::default();
329 }
330 }
331 self.last_modified = Instant::now();
332 }
333
334 pub fn clear_line(&mut self) {
336 let row = self.cursor_position.0;
337 if row < self.lines.len() {
338 self.lines[row] = vec![ScreenCell::default(); self.columns];
339 }
340 self.last_modified = Instant::now();
341 }
342
343 pub fn scroll_up(&mut self) {
345 if !self.lines.is_empty() {
346 self.lines.pop_front();
347 self.ensure_line(self.cursor_position.0);
348 }
349 self.last_modified = Instant::now();
350 }
351
352 pub fn smart_truncate(&mut self, max_size: usize) {
354 let current_size = self.lines.len();
355
356 if current_size <= max_size {
357 return;
358 }
359
360 let to_remove = current_size - max_size;
362
363 let beginning_lines = max_size / 10; if to_remove <= beginning_lines {
367 for _ in 0..to_remove {
369 self.lines.pop_front();
370 }
371 } else {
372 let end_lines = max_size - beginning_lines - 1; let beginning: VecDeque<Vec<ScreenCell>> =
377 self.lines.drain(0..beginning_lines.min(self.lines.len())).collect();
378
379 let end_start_index = self.lines.len().saturating_sub(end_lines);
380 let end: VecDeque<Vec<ScreenCell>> = self.lines.drain(end_start_index..).collect();
381
382 self.lines.clear();
384
385 for line in beginning {
387 self.lines.push_back(line);
388 }
389
390 let mut marker_line = vec![ScreenCell::default(); self.columns];
392 let marker_text = " [... TRUNCATED OUTPUT ...] ";
393
394 for (i, c) in marker_text.chars().enumerate() {
395 if i < self.columns {
396 marker_line[i] = ScreenCell {
397 character: c,
398 style: CellStyle::BOLD.union(CellStyle::REVERSE),
399 ..ScreenCell::default()
400 };
401 }
402 }
403
404 self.lines.push_back(marker_line);
405
406 for line in end {
408 self.lines.push_back(line);
409 }
410 }
411
412 if self.cursor_position.0 >= self.lines.len() {
414 self.cursor_position.0 = self.lines.len().saturating_sub(1);
415 }
416
417 self.last_modified = Instant::now();
418 }
419
420 pub fn to_plain_text(&self) -> String {
422 let mut result = String::with_capacity(self.lines.len() * self.columns);
423
424 for line in &self.lines {
425 let line_text: String = line.iter().map(|cell| cell.character).collect();
426 result.push_str(&line_text);
427 result.push('\n');
428 }
429
430 result
431 }
432
433 pub fn display(&self) -> Vec<String> {
435 let mut result = Vec::with_capacity(self.lines.len());
436
437 for line in &self.lines {
438 let line_text: String = line.iter().map(|cell| cell.character).collect();
439
440 let trimmed = line_text.trim_end();
442 result.push(trimmed.to_string());
443 }
444
445 while let Some(last) = result.last() {
447 if last.is_empty() {
448 result.pop();
449 } else {
450 break;
451 }
452 }
453
454 result
455 }
456
457 pub fn last_modified(&self) -> Instant {
459 self.last_modified
460 }
461
462 pub fn time_since_last_modified(&self) -> f64 {
464 self.last_modified.elapsed().as_secs_f64()
465 }
466}
467
468#[derive(Clone)]
470pub struct TerminalPerformer {
471 screen: Arc<Mutex<Screen>>,
473 attributes: ScreenCellAttributes,
475 sgr_state: HashMap<u16, bool>,
477 current_hyperlink_id: Option<String>,
479 current_hyperlink_url: Option<String>,
481 osc_params: Vec<String>,
483}
484
485impl std::fmt::Debug for TerminalPerformer {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 f.debug_struct("TerminalPerformer")
489 .field("attributes", &self.attributes)
490 .field("hyperlink_id", &self.current_hyperlink_id)
491 .field("hyperlink_url", &self.current_hyperlink_url)
492 .finish_non_exhaustive()
493 }
494}
495
496impl TerminalPerformer {
497 pub fn new(screen: Arc<Mutex<Screen>>) -> Self {
499 Self {
500 screen,
501 attributes: ScreenCellAttributes::default(),
502 sgr_state: HashMap::new(),
503 current_hyperlink_id: None,
504 current_hyperlink_url: None,
505 osc_params: Vec::new(),
506 }
507 }
508
509 pub fn screen(&self) -> &Arc<Mutex<Screen>> {
511 &self.screen
512 }
513
514 fn reset_attributes(&mut self) {
516 self.attributes = ScreenCellAttributes::default();
517 self.sgr_state.clear();
518 }
519
520 fn reset_hyperlink(&mut self) {
522 self.current_hyperlink_id = None;
523 self.current_hyperlink_url = None;
524 self.attributes.style.set(CellStyle::HYPERLINK, false);
525 self.attributes.hyperlink_url = None;
526 }
527
528 fn track_sgr(&mut self, param: u16) {
529 self.sgr_state.insert(param, true);
530 }
531
532 fn untrack_sgr(&mut self, params: &[u16]) {
533 for param in params {
534 self.sgr_state.remove(param);
535 }
536 }
537
538 fn handle_sgr_params(&mut self, params: &vte::Params) {
540 if params.is_empty() {
541 self.reset_attributes();
542 return;
543 }
544
545 for param_values in params.iter().flatten() {
546 self.handle_sgr_param(*param_values);
547 }
548 }
549
550 fn handle_sgr_param(&mut self, param: u16) {
551 if self.handle_basic_sgr_style(param)
552 || self.handle_font_sgr(param)
553 || self.handle_color_sgr(param)
554 || self.handle_frame_sgr(param)
555 || self.handle_script_sgr(param)
556 {
557 return;
558 }
559
560 debug!("Unsupported SGR parameter: {}", param);
561 }
562
563 fn handle_basic_sgr_style(&mut self, param: u16) -> bool {
564 match param {
565 0 => self.reset_attributes(),
566 1 => self.attributes.style.set(CellStyle::BOLD, true),
567 2 => self.attributes.style.set(CellStyle::DIM, true),
568 3 => self.attributes.style.set(CellStyle::ITALIC, true),
569 4 => {
570 self.attributes.style.set(CellStyle::UNDERLINE, true);
571 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
572 }
573 5 | 6 => self.attributes.style.set(CellStyle::BLINK, true),
574 7 => self.attributes.style.set(CellStyle::REVERSE, true),
575 8 => self.attributes.style.set(CellStyle::CONCEAL, true),
576 9 => self.attributes.style.set(CellStyle::STRIKETHROUGH, true),
577 20 => self.attributes.style.set(CellStyle::FRAKTUR, true),
578 21 => {
579 self.attributes.style.set(CellStyle::UNDERLINE, true);
580 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, true);
581 }
582 22 => {
583 self.attributes.style.set(CellStyle::BOLD, false);
584 self.attributes.style.set(CellStyle::DIM, false);
585 self.untrack_sgr(&[1, 2]);
586 return true;
587 }
588 23 => {
589 self.attributes.style.set(CellStyle::ITALIC, false);
590 self.attributes.style.set(CellStyle::FRAKTUR, false);
591 self.untrack_sgr(&[3, 20]);
592 return true;
593 }
594 24 => {
595 self.attributes.style.set(CellStyle::UNDERLINE, false);
596 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
597 self.untrack_sgr(&[4, 21]);
598 return true;
599 }
600 25 => {
601 self.attributes.style.set(CellStyle::BLINK, false);
602 self.untrack_sgr(&[5, 6]);
603 return true;
604 }
605 27 => self.attributes.style.set(CellStyle::REVERSE, false),
606 28 => self.attributes.style.set(CellStyle::CONCEAL, false),
607 29 => self.attributes.style.set(CellStyle::STRIKETHROUGH, false),
608 _ => return false,
609 }
610
611 self.track_sgr(param);
612 true
613 }
614
615 fn handle_font_sgr(&mut self, param: u16) -> bool {
616 match param {
617 10 => self.attributes.font = 0,
618 11..=19 => self.attributes.font = (param - 10) as u8,
619 _ => return false,
620 }
621
622 self.track_sgr(param);
623 true
624 }
625
626 fn handle_color_sgr(&mut self, param: u16) -> bool {
627 match param {
628 26 | 38 | 48 => {}
629 30..=37 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 30)),
630 39 => self.attributes.fg_color = None,
631 40..=47 => self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 40)),
632 49 => self.attributes.bg_color = None,
633 90..=97 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 90 + 8)),
634 100..=107 => {
635 self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 100 + 8));
636 }
637 _ => return false,
638 }
639
640 true
641 }
642
643 fn handle_frame_sgr(&mut self, param: u16) -> bool {
644 match param {
645 51 => {
646 self.attributes.style.set(CellStyle::FRAMED, true);
647 self.attributes.style.set(CellStyle::ENCIRCLED, false);
648 }
649 52 => {
650 self.attributes.style.set(CellStyle::FRAMED, false);
651 self.attributes.style.set(CellStyle::ENCIRCLED, true);
652 }
653 53 => self.attributes.style.set(CellStyle::OVERLINED, true),
654 54 => {
655 self.attributes.style.set(CellStyle::FRAMED, false);
656 self.attributes.style.set(CellStyle::ENCIRCLED, false);
657 self.untrack_sgr(&[51, 52]);
658 return true;
659 }
660 55 => {
661 self.attributes.style.set(CellStyle::OVERLINED, false);
662 self.untrack_sgr(&[53]);
663 return true;
664 }
665 60..=65 => {}
666 _ => return false,
667 }
668
669 self.track_sgr(param);
670 true
671 }
672
673 fn handle_script_sgr(&mut self, param: u16) -> bool {
674 match param {
675 73 => {
676 self.attributes.style.set(CellStyle::SUPERSCRIPT, true);
677 self.attributes.style.set(CellStyle::SUBSCRIPT, false);
678 }
679 74 => {
680 self.attributes.style.set(CellStyle::SUBSCRIPT, true);
681 self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
682 }
683 75 => {
684 self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
685 self.attributes.style.set(CellStyle::SUBSCRIPT, false);
686 self.untrack_sgr(&[73, 74]);
687 return true;
688 }
689 _ => return false,
690 }
691
692 self.track_sgr(param);
693 true
694 }
695}
696
697impl TerminalPerformer {
699 fn handle_sgr_dispatch(&mut self, params: &vte::Params) {
701 self.handle_sgr_params(params);
703
704 let param_arrays: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
706
707 if param_arrays.len() >= 3 {
708 let mut i = 0;
709 while i < param_arrays.len() {
710 if param_arrays[i].len() == 1 {
711 if param_arrays[i][0] == 38 && i + 2 < param_arrays.len() {
712 if param_arrays[i + 1].len() == 1
714 && param_arrays[i + 1][0] == 5
715 && param_arrays[i + 2].len() == 1
716 {
717 let color = param_arrays[i + 2][0] as u8;
719 self.attributes.fg_color = Some(TerminalColor::Color256(color));
720 i += 3;
721 continue;
722 } else if param_arrays[i + 1].len() == 1
723 && param_arrays[i + 1][0] == 2
724 && i + 4 < param_arrays.len()
725 && param_arrays[i + 2].len() == 1
726 && param_arrays[i + 3].len() == 1
727 && param_arrays[i + 4].len() == 1
728 {
729 let r = param_arrays[i + 2][0] as u8;
731 let g = param_arrays[i + 3][0] as u8;
732 let b = param_arrays[i + 4][0] as u8;
733 self.attributes.fg_color = Some(TerminalColor::TrueColor { r, g, b });
734 i += 5;
735 continue;
736 }
737 } else if param_arrays[i][0] == 48 && i + 2 < param_arrays.len() {
738 if param_arrays[i + 1].len() == 1
740 && param_arrays[i + 1][0] == 5
741 && param_arrays[i + 2].len() == 1
742 {
743 let color = param_arrays[i + 2][0] as u8;
745 self.attributes.bg_color = Some(TerminalColor::Color256(color));
746 i += 3;
747 continue;
748 } else if param_arrays[i + 1].len() == 1
749 && param_arrays[i + 1][0] == 2
750 && i + 4 < param_arrays.len()
751 && param_arrays[i + 2].len() == 1
752 && param_arrays[i + 3].len() == 1
753 && param_arrays[i + 4].len() == 1
754 {
755 let r = param_arrays[i + 2][0] as u8;
757 let g = param_arrays[i + 3][0] as u8;
758 let b = param_arrays[i + 4][0] as u8;
759 self.attributes.bg_color = Some(TerminalColor::TrueColor { r, g, b });
760 i += 5;
761 continue;
762 }
763 }
764 }
765 i += 1;
766 }
767 }
768 }
769
770 fn handle_osc_params(&mut self, params: &[&[u8]], _bell_terminated: bool) {
772 if params.is_empty() {
773 return;
774 }
775
776 let param_strings: Vec<String> =
778 params.iter().map(|p| String::from_utf8_lossy(p).to_string()).collect();
779
780 if param_strings.is_empty() {
781 return;
782 }
783
784 if param_strings[0] == "8" && param_strings.len() >= 3 {
786 let params =
791 if param_strings.len() > 1 { param_strings[1].clone() } else { String::new() };
792
793 let url =
794 if param_strings.len() > 2 { param_strings[2].clone() } else { String::new() };
795
796 let mut hyperlink_id = None;
798 for param in params.split(':') {
799 let parts: Vec<&str> = param.split('=').collect();
800 if parts.len() == 2 && parts[0] == "id" {
801 hyperlink_id = Some(parts[1].to_string());
802 }
803 }
804
805 if url.is_empty() {
807 self.reset_hyperlink();
809 } else {
810 self.attributes.style.set(CellStyle::HYPERLINK, true);
812 self.attributes.hyperlink_url = Some(url.clone());
813 self.current_hyperlink_url = Some(url);
814
815 if let Some(id) = hyperlink_id {
816 self.current_hyperlink_id = Some(id);
817 }
818 }
819 }
820 }
822
823 fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 {
824 params.iter().nth(index).and_then(|p| p.first().copied()).unwrap_or(default)
825 }
826
827 fn handle_cursor_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
828 let n = usize::from(Self::csi_param(params, 0, 1));
829 let current_row = screen.cursor_row();
830 let current_col = screen.cursor_col();
831
832 match c {
833 'A' => screen.move_cursor(current_row.saturating_sub(n), current_col),
834 'B' => screen.move_cursor(current_row + n, current_col),
835 'C' => screen.move_cursor(current_row, current_col + n),
836 'D' => screen.move_cursor(current_row, current_col.saturating_sub(n)),
837 'H' | 'f' => {
838 let row = usize::from(Self::csi_param(params, 0, 1)).saturating_sub(1);
839 let col = usize::from(Self::csi_param(params, 1, 1)).saturating_sub(1);
840 screen.move_cursor(row, col);
841 }
842 _ => return false,
843 }
844
845 true
846 }
847
848 fn clear_line_to_cursor(screen: &mut Screen) {
849 let row = screen.cursor_row();
850 let col = screen.cursor_col();
851
852 if row < screen.lines.len() {
853 for i in 0..=col.min(screen.lines[row].len().saturating_sub(1)) {
854 screen.lines[row][i] = ScreenCell::default();
855 }
856 }
857 }
858
859 fn handle_erase_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
860 match c {
861 'J' => Self::handle_erase_display(screen, Self::csi_param(params, 0, 0)),
862 'K' => Self::handle_erase_line(screen, Self::csi_param(params, 0, 0)),
863 _ => return false,
864 }
865
866 true
867 }
868
869 fn handle_erase_display(screen: &mut Screen, mode: u16) {
870 match mode {
871 0 => {
872 screen.clear_line_forward();
873 let row = screen.cursor_row();
874 if row + 1 < screen.lines.len() {
875 for i in row + 1..screen.lines.len() {
876 screen.lines[i] = vec![ScreenCell::default(); screen.columns];
877 }
878 }
879 }
880 1 => {
881 Self::clear_line_to_cursor(screen);
882 for i in 0..screen.cursor_row() {
883 if i < screen.lines.len() {
884 screen.lines[i] = vec![ScreenCell::default(); screen.columns];
885 }
886 }
887 }
888 2 | 3 => screen.clear(),
889 _ => debug!("Unhandled erase in display: {}", mode),
890 }
891 }
892
893 fn handle_erase_line(screen: &mut Screen, mode: u16) {
894 match mode {
895 0 => screen.clear_line_forward(),
896 1 => Self::clear_line_to_cursor(screen),
897 2 => screen.clear_line(),
898 _ => debug!("Unhandled erase in line: {}", mode),
899 }
900 }
901
902 fn handle_scroll_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
903 let n = usize::from(Self::csi_param(params, 0, 1));
904
905 match c {
906 'S' => {
907 for _ in 0..n {
908 screen.scroll_up();
909 }
910 }
911 'T' => {
912 let columns = screen.columns;
913 for _ in 0..n {
914 screen.lines.push_front(vec![ScreenCell::default(); columns]);
915 if screen.lines.len() > screen.max_lines {
916 screen.lines.pop_back();
917 }
918 }
919 screen.move_cursor(screen.cursor_row() + n, screen.cursor_col());
920 }
921 _ => return false,
922 }
923
924 true
925 }
926}
927
928impl Perform for TerminalPerformer {
930 fn print(&mut self, c: char) {
931 if let Ok(mut screen) = self.screen.lock() {
932 screen.put_char(c, self.attributes.clone());
933 } else {
934 warn!("Failed to lock screen for print");
935 }
936 }
937
938 fn execute(&mut self, byte: u8) {
939 if let Ok(mut screen) = self.screen.lock() {
940 match byte {
941 b'\r' => screen.carriage_return(),
942 b'\n' => {
943 screen.carriage_return();
944 screen.linefeed();
945 }
946 b'\t' => {
947 let current_col = screen.cursor_col();
949 let new_col = (current_col + 8) & !7;
950 let current_row = screen.cursor_row();
952 screen.move_cursor(current_row, new_col);
953 }
954 b'\x08' => {
955 if screen.cursor_col() > 0 {
957 let current_row = screen.cursor_row();
958 let new_col = screen.cursor_col() - 1;
959 screen.move_cursor(current_row, new_col);
960 }
961 }
962 b'\x0C' => {
963 screen.clear();
965 }
966 b'\x07' => { }
968 _ => {
969 debug!("Unhandled execute: {:?}", byte);
970 }
971 }
972 } else {
973 warn!("Failed to lock screen for execute");
974 }
975 }
976
977 fn hook(&mut self, _params: &vte::Params, _intermediates: &[u8], _ignore: bool, _c: char) {
978 }
980
981 fn put(&mut self, _byte: u8) {
982 }
984
985 fn unhook(&mut self) {
986 }
988
989 fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
990 self.handle_osc_params(params, bell_terminated);
992 }
993
994 fn csi_dispatch(
995 &mut self,
996 params: &vte::Params,
997 _intermediates: &[u8],
998 _ignore: bool,
999 c: char,
1000 ) {
1001 if c == 'm' {
1003 self.handle_sgr_dispatch(params);
1004 return;
1005 }
1006
1007 if let Ok(mut screen) = self.screen.lock() {
1008 if Self::handle_cursor_csi(&mut screen, params, c)
1009 || Self::handle_erase_csi(&mut screen, params, c)
1010 || Self::handle_scroll_csi(&mut screen, params, c)
1011 {
1012 return;
1013 }
1014
1015 debug!("Unhandled CSI: {:?} {:?}", params, c);
1016 } else {
1017 warn!("Failed to lock screen for csi_dispatch");
1018 }
1019 }
1020
1021 fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
1022 if intermediates.is_empty() {
1023 match byte {
1024 b'c' => {
1025 if let Ok(mut screen) = self.screen.lock() {
1027 screen.clear();
1028 }
1029 self.reset_attributes();
1030 }
1031 b'7' | b'8' => {
1032 }
1035 _ => debug!("Unhandled ESC dispatch: {:?}", byte),
1036 }
1037 }
1038 }
1039}
1040
1041#[derive(Clone)]
1043pub struct TerminalEmulator {
1044 performer: TerminalPerformer,
1046 screen: Arc<Mutex<Screen>>,
1048}
1049
1050impl std::fmt::Debug for TerminalEmulator {
1052 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1053 f.debug_struct("TerminalEmulator")
1054 .field("performer", &self.performer)
1055 .finish_non_exhaustive()
1056 }
1057}
1058
1059impl TerminalEmulator {
1060 pub fn new(columns: usize) -> Self {
1062 let screen = Arc::new(Mutex::new(Screen::new(columns)));
1063 let performer = TerminalPerformer::new(screen.clone());
1064
1065 Self { performer, screen }
1066 }
1067
1068 pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
1070 let screen = Arc::new(Mutex::new(Screen::new_with_max_lines(columns, max_lines)));
1071 let performer = TerminalPerformer::new(screen.clone());
1072
1073 Self { performer, screen }
1074 }
1075
1076 pub fn process(&mut self, data: &str) {
1078 let mut parser = Parser::new();
1079
1080 let chunk_size = 4096;
1082 let data_bytes = data.as_bytes();
1083
1084 for chunk in data_bytes.chunks(chunk_size) {
1085 parser.advance(&mut self.performer, chunk);
1086 }
1087 }
1088
1089 pub fn process_with_limited_buffer(&mut self, data: &str, max_lines: usize) {
1091 if let Ok(mut screen) = self.screen.lock() {
1092 screen.max_lines = max_lines.min(MAX_SCREEN_LINES);
1094 }
1095
1096 self.process(data);
1097
1098 if let Ok(mut screen) = self.screen.lock() {
1100 if screen.lines.len() > max_lines {
1101 screen.smart_truncate(max_lines);
1102 }
1103 }
1104 }
1105
1106 pub fn get_screen(&self) -> Arc<Mutex<Screen>> {
1108 self.screen.clone()
1109 }
1110
1111 pub fn display(&self) -> Vec<String> {
1113 if let Ok(screen) = self.screen.lock() {
1114 screen.display()
1115 } else {
1116 warn!("Failed to lock screen for display");
1117 vec![]
1118 }
1119 }
1120
1121 pub fn to_plain_text(&self) -> String {
1123 if let Ok(screen) = self.screen.lock() {
1124 screen.to_plain_text()
1125 } else {
1126 warn!("Failed to lock screen for to_plain_text");
1127 String::new()
1128 }
1129 }
1130
1131 pub fn clear(&mut self) {
1133 if let Ok(mut screen) = self.screen.lock() {
1134 screen.clear();
1135 } else {
1136 warn!("Failed to lock screen for clear");
1137 }
1138 }
1139}
1140
1141type CacheEntryMap = HashMap<u64, (Vec<String>, Instant)>;
1148
1149fn hash_terminal_text(text: &str) -> u64 {
1151 use std::hash::{Hash, Hasher};
1152 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1153 text.hash(&mut hasher);
1154 hasher.finish()
1155}
1156
1157#[derive(Debug, Default)]
1160struct CacheInner {
1161 map: CacheEntryMap,
1162 order: VecDeque<u64>,
1164}
1165
1166#[derive(Debug, Clone)]
1168struct TerminalCache {
1169 inner: Arc<RwLock<CacheInner>>,
1170 max_entries: usize,
1172 ttl: u64,
1174}
1175
1176impl TerminalCache {
1177 fn new(max_entries: usize, ttl: u64) -> Self {
1179 Self { inner: Arc::new(RwLock::new(CacheInner::default())), max_entries, ttl }
1180 }
1181
1182 fn get(&self, text: &str) -> Option<Vec<String>> {
1184 let key = hash_terminal_text(text);
1185 let inner = self.inner.read().ok()?;
1186 let (value, timestamp) = inner.map.get(&key)?;
1187 if timestamp.elapsed().as_secs() < self.ttl {
1188 Some(value.clone())
1189 } else {
1190 None
1191 }
1192 }
1193
1194 fn insert(&self, text: &str, value: Vec<String>) {
1196 let key = hash_terminal_text(text);
1197 let Ok(mut inner) = self.inner.write() else {
1198 return;
1199 };
1200 if inner.map.insert(key, (value, Instant::now())).is_none() {
1201 inner.order.push_back(key);
1202 }
1203 while inner.order.len() > self.max_entries {
1204 let Some(old) = inner.order.pop_front() else {
1205 break;
1206 };
1207 inner.map.remove(&old);
1208 }
1209 }
1210
1211 fn cleanup(&self) {
1213 let Ok(mut inner) = self.inner.write() else {
1214 return;
1215 };
1216 let ttl = self.ttl;
1217 let CacheInner { map, order } = &mut *inner;
1218 map.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < ttl);
1219 order.retain(|key| map.contains_key(key));
1220 }
1221}
1222
1223lazy_static::lazy_static! {
1225 static ref TERMINAL_CACHE: TerminalCache = TerminalCache::new(100, CACHE_TTL);
1226}
1227
1228#[derive(Debug, Clone)]
1230pub struct TerminalOutputDiff {
1231 previous_output: Vec<String>,
1233 output_hash: String,
1235 max_lines: usize,
1237}
1238
1239impl Default for TerminalOutputDiff {
1240 fn default() -> Self {
1241 Self::new()
1242 }
1243}
1244
1245impl TerminalOutputDiff {
1246 pub fn new() -> Self {
1248 Self { previous_output: Vec::new(), output_hash: String::new(), max_lines: 1000 }
1249 }
1250
1251 pub fn new_with_max_lines(max_lines: usize) -> Self {
1253 Self { previous_output: Vec::new(), output_hash: String::new(), max_lines }
1254 }
1255
1256 pub fn detect_changes(&mut self, new_output: &[String]) -> Vec<String> {
1258 if self.previous_output.is_empty() {
1259 self.previous_output = new_output.to_vec();
1261 self.output_hash = self.calculate_hash(new_output);
1262 return new_output.to_vec();
1263 }
1264
1265 let new_hash = self.calculate_hash(new_output);
1267 if new_hash == self.output_hash {
1268 return Vec::new(); }
1270
1271 let mut changes = Vec::new();
1273
1274 let nold = self.previous_output.len().min(self.max_lines);
1276 let nnew = new_output.len().min(self.max_lines);
1277
1278 let mut matched_position = None;
1280
1281 let is_prefix = nold <= nnew && (0..nold).all(|i| self.previous_output[i] == new_output[i]);
1283
1284 if is_prefix {
1285 matched_position = Some(nold);
1287 } else {
1288 let mut best_match = 0;
1290 let mut best_position = 0;
1291
1292 let window_size = 3.min(nold); if window_size > 0 {
1296 for i in (0..=nnew.saturating_sub(window_size)).rev() {
1297 let mut match_count = 0;
1299 for j in 0..window_size {
1300 if i + j < nnew
1301 && nold.saturating_sub(window_size) + j < nold
1302 && new_output[i + j]
1303 == self.previous_output[nold.saturating_sub(window_size) + j]
1304 {
1305 match_count += 1;
1306 }
1307 }
1308
1309 if match_count > best_match {
1310 best_match = match_count;
1311 best_position = i + window_size;
1312
1313 if best_match == window_size {
1314 break;
1316 }
1317 }
1318 }
1319 }
1320
1321 if best_match >= window_size / 2 {
1322 matched_position = Some(best_position);
1324 }
1325 }
1326
1327 if let Some(pos) = matched_position {
1329 if pos < nnew {
1330 changes = new_output[pos..].to_vec();
1331
1332 if !changes.is_empty()
1334 && !self.previous_output.is_empty()
1335 && changes[0] == self.previous_output[self.previous_output.len() - 1]
1336 {
1337 changes.remove(0);
1338 }
1339 }
1340 } else {
1341 changes = new_output.to_vec();
1343 }
1344
1345 self.previous_output = new_output.to_vec();
1347 self.output_hash = new_hash;
1348
1349 changes
1350 }
1351
1352 fn calculate_hash(&self, lines: &[String]) -> String {
1354 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1357 for line in lines.iter().take(self.max_lines) {
1358 std::hash::Hash::hash(line, &mut hasher);
1359 }
1360 format!("{:x}", std::hash::Hasher::finish(&hasher))
1361 }
1362
1363 pub fn reset(&mut self) {
1365 self.previous_output.clear();
1366 self.output_hash.clear();
1367 }
1368}
1369
1370pub fn render_terminal_output(text: &str) -> Vec<String> {
1372 if let Some(cached) = TERMINAL_CACHE.get(text) {
1374 return cached;
1375 }
1376
1377 let mut terminal = TerminalEmulator::new(DEFAULT_COLUMNS);
1378
1379 if text.len() > MAX_OUTPUT_SIZE {
1381 terminal.process_with_limited_buffer(text, DEFAULT_MAX_SCREEN_LINES);
1383 } else {
1384 terminal.process(text);
1385 }
1386
1387 let result = terminal.display();
1388
1389 if text.len() < MAX_OUTPUT_SIZE {
1391 TERMINAL_CACHE.insert(text, result.clone());
1392 }
1393
1394 if rand::random::<u32>() % 100 == 0 {
1396 TERMINAL_CACHE.cleanup();
1397 }
1398
1399 result.into_iter().map(|line| strip_ansi_codes(&line)).collect()
1402}
1403
1404pub fn incremental_text(text: &str, last_pending_output: &str) -> String {
1406 if text.is_empty() {
1408 return String::new();
1409 }
1410
1411 if last_pending_output.is_empty() {
1413 let lines = render_terminal_output(text);
1415 return lines.join("\n").trim().to_string();
1416 }
1417
1418 let is_append = text.starts_with(last_pending_output);
1420
1421 if is_append && text.len() > last_pending_output.len() {
1422 let new_part = &text[last_pending_output.len()..];
1424
1425 let context_len = 200.min(last_pending_output.len());
1429 let full_context = if context_len > 0 {
1430 let start_pos = crate::utils::floor_char_boundary(
1431 last_pending_output,
1432 last_pending_output.len() - context_len,
1433 );
1434 format!("{}{}", &last_pending_output[start_pos..], new_part)
1435 } else {
1436 new_part.to_string()
1437 };
1438
1439 let previous_lines = render_terminal_output(last_pending_output);
1441 let combined_lines = render_terminal_output(&full_context);
1442
1443 let mut diff_detector = TerminalOutputDiff::new();
1445 diff_detector.previous_output = previous_lines;
1446
1447 let changes = diff_detector.detect_changes(&combined_lines);
1449
1450 if changes.is_empty() {
1451 return String::new();
1452 }
1453
1454 return changes.join("\n");
1455 }
1456
1457 let text_limit = if text.len() > MAX_OUTPUT_SIZE {
1461 let start_offset = text.len() - MAX_OUTPUT_SIZE;
1462
1463 let adjusted_offset =
1465 text[start_offset..].find('\n').map_or(start_offset, |pos| start_offset + pos + 1);
1466
1467 &text[adjusted_offset..]
1468 } else {
1469 text
1470 };
1471
1472 let previous_lines = render_terminal_output(last_pending_output);
1474 let new_lines = render_terminal_output(text_limit);
1475
1476 let mut diff_detector = TerminalOutputDiff::new();
1478 diff_detector.previous_output = previous_lines;
1479
1480 let changes = diff_detector.detect_changes(&new_lines);
1482
1483 if changes.is_empty() {
1484 return String::new();
1485 }
1486
1487 changes.join("\n")
1488}
1489
1490pub fn strip_ansi_codes(input: &str) -> String {
1492 static RE: std::sync::OnceLock<Option<Regex>> = std::sync::OnceLock::new();
1493
1494 if !input.contains('\u{1b}') {
1496 return input.to_string();
1497 }
1498
1499 let re = RE.get_or_init(|| {
1509 Regex::new(
1510 r"\x1b\[[0-9;:?<>=!]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[=>MNcD78]",
1511 )
1512 .ok()
1513 });
1514 let cleaned = match re {
1515 Some(re) => re.replace_all(input, "").into_owned(),
1516 None => input.to_string(),
1517 };
1518 cleaned.replace('\u{1b}', "")
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use super::*;
1525
1526 #[test]
1527 fn render_applies_cursor_left_overwrite() {
1528 let rendered = render_terminal_output("ZZZZZZZZZZ\u{1b}[5DQQQQQ").join("\n");
1531 assert!(rendered.contains("ZZZZZQQQQQ"), "cursor-left not applied; got {rendered:?}");
1532 assert!(
1534 !rendered.contains("ZZZZZZZZZZQQQQQ"),
1535 "looks like a plain strip; got {rendered:?}"
1536 );
1537 }
1538
1539 #[test]
1540 fn strips_csi_osc_and_cursor_sequences() {
1541 assert_eq!(strip_ansi_codes("\u{1b}[1;35mhi\u{1b}[0m"), "hi");
1543 assert_eq!(strip_ansi_codes("\u{1b}[?2004h>>> \u{1b}[?2004l\u{1b}[4D42"), ">>> 42");
1545 assert_eq!(strip_ansi_codes("\u{1b}]0;user@host\u{7}prompt$ "), "prompt$ ");
1547 assert_eq!(strip_ansi_codes("no escapes here"), "no escapes here");
1549 }
1550
1551 #[test]
1552 fn test_screen_basic_operations() {
1553 let mut screen = Screen::new(80);
1554
1555 let _attributes = ScreenCellAttributes::default();
1557
1558 screen.put_char_basic('H', CellStyle::default(), None, None);
1560 screen.put_char_basic('e', CellStyle::default(), None, None);
1561 screen.put_char_basic('l', CellStyle::default(), None, None);
1562 screen.put_char_basic('l', CellStyle::default(), None, None);
1563 screen.put_char_basic('o', CellStyle::default(), None, None);
1564
1565 let display = screen.display();
1566 assert_eq!(display, vec!["Hello"]);
1567
1568 screen.carriage_return();
1570 screen.linefeed();
1571
1572 screen.put_char_basic('W', CellStyle::default(), None, None);
1573 screen.put_char_basic('o', CellStyle::default(), None, None);
1574 screen.put_char_basic('r', CellStyle::default(), None, None);
1575 screen.put_char_basic('l', CellStyle::default(), None, None);
1576 screen.put_char_basic('d', CellStyle::default(), None, None);
1577
1578 let display = screen.display();
1579 assert_eq!(display, vec!["Hello", "World"]);
1580
1581 screen.clear_line();
1583 let display = screen.display();
1584 assert_eq!(display, vec!["Hello"]);
1585 }
1586
1587 #[test]
1588 fn test_terminal_emulator_basic() {
1589 let mut terminal = TerminalEmulator::new(80);
1590
1591 terminal.process("Hello\r\nWorld");
1593 let display = terminal.display();
1594 assert_eq!(display, vec!["Hello", "World"]);
1595
1596 terminal.clear();
1598 terminal.process("Normal \x1b[1mBold\x1b[0m Normal");
1599 let display = terminal.display();
1600 assert_eq!(display, vec!["Normal Bold Normal"]);
1601
1602 terminal.clear();
1604 terminal.process("Hello\x1b[5D_\x1b[1C_\x1b[1C_");
1605 let display = terminal.display();
1606 assert_eq!(display, vec!["_e_l_"]);
1607 }
1608
1609 #[test]
1610 fn test_incremental_output() {
1611 let old = vec!["Line 1".to_string(), "Line 2".to_string()];
1612 let new = vec!["Line 1".to_string(), "Line 2".to_string(), "Line 3".to_string()];
1613
1614 let mut diff_detector = TerminalOutputDiff::new();
1615 diff_detector.previous_output = old;
1616
1617 let incremental = diff_detector.detect_changes(&new);
1618 assert_eq!(incremental, vec!["Line 3"]);
1619
1620 let old = vec!["Line A".to_string(), "Line B".to_string()];
1622 let new = vec!["Line X".to_string(), "Line Y".to_string()];
1623
1624 let mut diff_detector = TerminalOutputDiff::new();
1625 diff_detector.previous_output = old;
1626
1627 let incremental = diff_detector.detect_changes(&new);
1628 assert_eq!(incremental, vec!["Line X", "Line Y"]);
1629 }
1630
1631 #[test]
1632 fn test_render_terminal_output() {
1633 let text = "Hello\r\nWorld\r\n\x1b[31mRed\x1b[0m Text";
1634 let lines = render_terminal_output(text);
1635 assert_eq!(lines, vec!["Hello", "World", "Red Text"]);
1636 }
1637
1638 #[test]
1639 fn test_smart_truncate() {
1640 let mut screen = Screen::new_with_max_lines(80, 20);
1641
1642 for i in 0..30 {
1644 let line = format!("Line {i}");
1645 for c in line.chars() {
1646 screen.put_char(c, ScreenCellAttributes::default());
1647 }
1648 screen.carriage_return();
1649 screen.linefeed();
1650 }
1651
1652 assert_eq!(screen.lines.len(), 20);
1654
1655 screen.smart_truncate(10);
1657
1658 assert_eq!(screen.lines.len(), 10);
1660
1661 let has_truncation_marker = screen.lines.iter().any(|line| {
1663 let line_text: String = line.iter().map(|cell| cell.character).collect();
1664 line_text.contains("TRUNCATED")
1665 });
1666
1667 assert!(has_truncation_marker);
1668 }
1669
1670 #[test]
1671 fn test_terminal_cache() {
1672 let cache = TerminalCache::new(10, 60);
1673
1674 cache.insert("test", vec!["line1".to_string(), "line2".to_string()]);
1676
1677 let retrieved = cache.get("test");
1679 assert_eq!(retrieved, Some(vec!["line1".to_string(), "line2".to_string()]));
1680
1681 let not_found = cache.get("unknown");
1683 assert_eq!(not_found, None);
1684 }
1685
1686 #[test]
1687 fn test_incremental_text_append() {
1688 let old_text = "Line 1\nLine 2\n";
1689 let new_text = "Line 1\nLine 2\nLine 3\n";
1690
1691 let incremental = incremental_text(new_text, old_text);
1692 assert_eq!(incremental, "Line 3");
1693 }
1694
1695 #[test]
1696 fn test_terminal_color_handling() {
1697 let mut terminal = TerminalEmulator::new(80);
1698
1699 terminal.process("\x1b[31mRed\x1b[32mGreen\x1b[0mNormal");
1701 let display = terminal.display();
1702 assert_eq!(display, vec!["RedGreenNormal"]);
1703
1704 terminal.clear();
1706 terminal.process("\x1b[38;5;208mOrange\x1b[0mNormal");
1707 let display = terminal.display();
1708 assert_eq!(display, vec!["OrangeNormal"]);
1709 }
1710}