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 mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
175 lines.push_back(vec![ScreenCell::default(); columns]);
176
177 Self {
178 lines,
179 cursor_position: (0, 0),
180 columns,
181 cursor_visible: true,
182 max_lines: DEFAULT_MAX_SCREEN_LINES,
183 last_modified: Instant::now(),
184 }
185 }
186
187 pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
189 let mut lines = VecDeque::with_capacity(max_lines.min(MAX_SCREEN_LINES));
190 lines.push_back(vec![ScreenCell::default(); columns]);
191
192 Self {
193 lines,
194 cursor_position: (0, 0),
195 columns,
196 cursor_visible: true,
197 max_lines: max_lines.min(MAX_SCREEN_LINES),
198 last_modified: Instant::now(),
199 }
200 }
201
202 pub fn cursor_row(&self) -> usize {
204 self.cursor_position.0
205 }
206
207 pub fn cursor_col(&self) -> usize {
209 self.cursor_position.1
210 }
211
212 fn ensure_line(&mut self, line_idx: usize) {
214 while self.lines.len() <= line_idx {
216 self.lines.push_back(vec![ScreenCell::default(); self.columns]);
217 }
218
219 while self.lines.len() > self.max_lines {
221 self.lines.pop_front();
222
223 if self.cursor_position.0 > 0 {
225 self.cursor_position.0 -= 1;
226 }
227 }
228
229 self.last_modified = Instant::now();
230 }
231
232 fn ensure_cursor_position(&mut self) {
234 self.ensure_line(self.cursor_position.0);
235
236 if self.cursor_position.1 >= self.columns {
238 self.cursor_position.1 = self.columns - 1;
239 }
240
241 self.last_modified = Instant::now();
242 }
243
244 pub fn put_char(&mut self, c: char, attributes: ScreenCellAttributes) {
246 self.ensure_cursor_position();
247
248 let row = self.cursor_position.0;
250 let col = self.cursor_position.1;
251
252 if col < self.lines[row].len() {
254 self.lines[row][col] = ScreenCell::new(c, attributes);
255 } else {
256 while self.lines[row].len() <= col {
258 self.lines[row].push(ScreenCell::default());
259 }
260 self.lines[row][col] = ScreenCell::new(c, attributes);
261 }
262
263 self.cursor_position.1 += 1;
265 if self.cursor_position.1 >= self.columns {
266 self.cursor_position.1 = 0;
267 self.cursor_position.0 += 1;
268 self.ensure_cursor_position();
269 }
270
271 self.last_modified = Instant::now();
272 }
273
274 #[allow(clippy::too_many_arguments)]
276 pub fn put_char_basic(
277 &mut self,
278 c: char,
279 style: CellStyle,
280 fg_color: Option<TerminalColor>,
281 bg_color: Option<TerminalColor>,
282 ) {
283 let attributes = ScreenCellAttributes { style, fg_color, bg_color, ..Default::default() };
284
285 self.put_char(c, attributes);
286 }
287
288 pub fn move_cursor(&mut self, row: usize, col: usize) {
290 self.cursor_position = (row, col);
291 self.ensure_cursor_position();
292 self.last_modified = Instant::now();
293 }
294
295 pub fn linefeed(&mut self) {
297 self.cursor_position.0 += 1;
298 self.ensure_cursor_position();
299 self.last_modified = Instant::now();
300 }
301
302 pub fn carriage_return(&mut self) {
304 self.cursor_position.1 = 0;
305 self.last_modified = Instant::now();
306 }
307
308 pub fn clear(&mut self) {
310 self.lines.clear();
311 self.lines.push_back(vec![ScreenCell::default(); self.columns]);
312 self.cursor_position = (0, 0);
313 self.last_modified = Instant::now();
314 }
315
316 pub fn clear_line_forward(&mut self) {
318 let row = self.cursor_position.0;
319 let col = self.cursor_position.1;
320
321 if row < self.lines.len() {
322 for i in col..self.lines[row].len() {
323 self.lines[row][i] = ScreenCell::default();
324 }
325 }
326 self.last_modified = Instant::now();
327 }
328
329 pub fn clear_line(&mut self) {
331 let row = self.cursor_position.0;
332 if row < self.lines.len() {
333 self.lines[row] = vec![ScreenCell::default(); self.columns];
334 }
335 self.last_modified = Instant::now();
336 }
337
338 pub fn scroll_up(&mut self) {
340 if !self.lines.is_empty() {
341 self.lines.pop_front();
342 self.ensure_line(self.cursor_position.0);
343 }
344 self.last_modified = Instant::now();
345 }
346
347 pub fn smart_truncate(&mut self, max_size: usize) {
349 let current_size = self.lines.len();
350
351 if current_size <= max_size {
352 return;
353 }
354
355 let to_remove = current_size - max_size;
357
358 let beginning_lines = max_size / 10; if to_remove <= beginning_lines {
362 for _ in 0..to_remove {
364 self.lines.pop_front();
365 }
366 } else {
367 let end_lines = max_size - beginning_lines - 1; let beginning: VecDeque<Vec<ScreenCell>> =
372 self.lines.drain(0..beginning_lines.min(self.lines.len())).collect();
373
374 let end_start_index = self.lines.len().saturating_sub(end_lines);
375 let end: VecDeque<Vec<ScreenCell>> = self.lines.drain(end_start_index..).collect();
376
377 self.lines.clear();
379
380 for line in beginning {
382 self.lines.push_back(line);
383 }
384
385 let mut marker_line = vec![ScreenCell::default(); self.columns];
387 let marker_text = " [... TRUNCATED OUTPUT ...] ";
388
389 for (i, c) in marker_text.chars().enumerate() {
390 if i < self.columns {
391 marker_line[i] = ScreenCell {
392 character: c,
393 style: CellStyle::BOLD.union(CellStyle::REVERSE),
394 ..ScreenCell::default()
395 };
396 }
397 }
398
399 self.lines.push_back(marker_line);
400
401 for line in end {
403 self.lines.push_back(line);
404 }
405 }
406
407 if self.cursor_position.0 >= self.lines.len() {
409 self.cursor_position.0 = self.lines.len().saturating_sub(1);
410 }
411
412 self.last_modified = Instant::now();
413 }
414
415 pub fn to_plain_text(&self) -> String {
417 let mut result = String::with_capacity(self.lines.len() * self.columns);
418
419 for line in &self.lines {
420 let line_text: String = line.iter().map(|cell| cell.character).collect();
421 result.push_str(&line_text);
422 result.push('\n');
423 }
424
425 result
426 }
427
428 pub fn display(&self) -> Vec<String> {
430 let mut result = Vec::with_capacity(self.lines.len());
431
432 for line in &self.lines {
433 let line_text: String = line.iter().map(|cell| cell.character).collect();
434
435 let trimmed = line_text.trim_end();
437 result.push(trimmed.to_string());
438 }
439
440 while let Some(last) = result.last() {
442 if last.is_empty() {
443 result.pop();
444 } else {
445 break;
446 }
447 }
448
449 result
450 }
451
452 pub fn last_modified(&self) -> Instant {
454 self.last_modified
455 }
456
457 pub fn time_since_last_modified(&self) -> f64 {
459 self.last_modified.elapsed().as_secs_f64()
460 }
461}
462
463#[derive(Clone)]
465pub struct TerminalPerformer {
466 screen: Arc<Mutex<Screen>>,
468 attributes: ScreenCellAttributes,
470 sgr_state: HashMap<u16, bool>,
472 current_hyperlink_id: Option<String>,
474 current_hyperlink_url: Option<String>,
476 osc_params: Vec<String>,
478}
479
480impl std::fmt::Debug for TerminalPerformer {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("TerminalPerformer")
484 .field("attributes", &self.attributes)
485 .field("hyperlink_id", &self.current_hyperlink_id)
486 .field("hyperlink_url", &self.current_hyperlink_url)
487 .finish_non_exhaustive()
488 }
489}
490
491impl TerminalPerformer {
492 pub fn new(screen: Arc<Mutex<Screen>>) -> Self {
494 Self {
495 screen,
496 attributes: ScreenCellAttributes::default(),
497 sgr_state: HashMap::new(),
498 current_hyperlink_id: None,
499 current_hyperlink_url: None,
500 osc_params: Vec::new(),
501 }
502 }
503
504 pub fn screen(&self) -> &Arc<Mutex<Screen>> {
506 &self.screen
507 }
508
509 fn reset_attributes(&mut self) {
511 self.attributes = ScreenCellAttributes::default();
512 self.sgr_state.clear();
513 }
514
515 fn reset_hyperlink(&mut self) {
517 self.current_hyperlink_id = None;
518 self.current_hyperlink_url = None;
519 self.attributes.style.set(CellStyle::HYPERLINK, false);
520 self.attributes.hyperlink_url = None;
521 }
522
523 fn track_sgr(&mut self, param: u16) {
524 self.sgr_state.insert(param, true);
525 }
526
527 fn untrack_sgr(&mut self, params: &[u16]) {
528 for param in params {
529 self.sgr_state.remove(param);
530 }
531 }
532
533 fn handle_sgr_params(&mut self, params: &vte::Params) {
535 if params.is_empty() {
536 self.reset_attributes();
537 return;
538 }
539
540 for param_values in params.iter().flatten() {
541 self.handle_sgr_param(*param_values);
542 }
543 }
544
545 fn handle_sgr_param(&mut self, param: u16) {
546 if self.handle_basic_sgr_style(param)
547 || self.handle_font_sgr(param)
548 || self.handle_color_sgr(param)
549 || self.handle_frame_sgr(param)
550 || self.handle_script_sgr(param)
551 {
552 return;
553 }
554
555 debug!("Unsupported SGR parameter: {}", param);
556 }
557
558 fn handle_basic_sgr_style(&mut self, param: u16) -> bool {
559 match param {
560 0 => self.reset_attributes(),
561 1 => self.attributes.style.set(CellStyle::BOLD, true),
562 2 => self.attributes.style.set(CellStyle::DIM, true),
563 3 => self.attributes.style.set(CellStyle::ITALIC, true),
564 4 => {
565 self.attributes.style.set(CellStyle::UNDERLINE, true);
566 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
567 }
568 5 | 6 => self.attributes.style.set(CellStyle::BLINK, true),
569 7 => self.attributes.style.set(CellStyle::REVERSE, true),
570 8 => self.attributes.style.set(CellStyle::CONCEAL, true),
571 9 => self.attributes.style.set(CellStyle::STRIKETHROUGH, true),
572 20 => self.attributes.style.set(CellStyle::FRAKTUR, true),
573 21 => {
574 self.attributes.style.set(CellStyle::UNDERLINE, true);
575 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, true);
576 }
577 22 => {
578 self.attributes.style.set(CellStyle::BOLD, false);
579 self.attributes.style.set(CellStyle::DIM, false);
580 self.untrack_sgr(&[1, 2]);
581 return true;
582 }
583 23 => {
584 self.attributes.style.set(CellStyle::ITALIC, false);
585 self.attributes.style.set(CellStyle::FRAKTUR, false);
586 self.untrack_sgr(&[3, 20]);
587 return true;
588 }
589 24 => {
590 self.attributes.style.set(CellStyle::UNDERLINE, false);
591 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
592 self.untrack_sgr(&[4, 21]);
593 return true;
594 }
595 25 => {
596 self.attributes.style.set(CellStyle::BLINK, false);
597 self.untrack_sgr(&[5, 6]);
598 return true;
599 }
600 27 => self.attributes.style.set(CellStyle::REVERSE, false),
601 28 => self.attributes.style.set(CellStyle::CONCEAL, false),
602 29 => self.attributes.style.set(CellStyle::STRIKETHROUGH, false),
603 _ => return false,
604 }
605
606 self.track_sgr(param);
607 true
608 }
609
610 fn handle_font_sgr(&mut self, param: u16) -> bool {
611 match param {
612 10 => self.attributes.font = 0,
613 11..=19 => self.attributes.font = (param - 10) as u8,
614 _ => return false,
615 }
616
617 self.track_sgr(param);
618 true
619 }
620
621 fn handle_color_sgr(&mut self, param: u16) -> bool {
622 match param {
623 26 | 38 | 48 => {}
624 30..=37 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 30)),
625 39 => self.attributes.fg_color = None,
626 40..=47 => self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 40)),
627 49 => self.attributes.bg_color = None,
628 90..=97 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 90 + 8)),
629 100..=107 => {
630 self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 100 + 8));
631 }
632 _ => return false,
633 }
634
635 true
636 }
637
638 fn handle_frame_sgr(&mut self, param: u16) -> bool {
639 match param {
640 51 => {
641 self.attributes.style.set(CellStyle::FRAMED, true);
642 self.attributes.style.set(CellStyle::ENCIRCLED, false);
643 }
644 52 => {
645 self.attributes.style.set(CellStyle::FRAMED, false);
646 self.attributes.style.set(CellStyle::ENCIRCLED, true);
647 }
648 53 => self.attributes.style.set(CellStyle::OVERLINED, true),
649 54 => {
650 self.attributes.style.set(CellStyle::FRAMED, false);
651 self.attributes.style.set(CellStyle::ENCIRCLED, false);
652 self.untrack_sgr(&[51, 52]);
653 return true;
654 }
655 55 => {
656 self.attributes.style.set(CellStyle::OVERLINED, false);
657 self.untrack_sgr(&[53]);
658 return true;
659 }
660 60..=65 => {}
661 _ => return false,
662 }
663
664 self.track_sgr(param);
665 true
666 }
667
668 fn handle_script_sgr(&mut self, param: u16) -> bool {
669 match param {
670 73 => {
671 self.attributes.style.set(CellStyle::SUPERSCRIPT, true);
672 self.attributes.style.set(CellStyle::SUBSCRIPT, false);
673 }
674 74 => {
675 self.attributes.style.set(CellStyle::SUBSCRIPT, true);
676 self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
677 }
678 75 => {
679 self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
680 self.attributes.style.set(CellStyle::SUBSCRIPT, false);
681 self.untrack_sgr(&[73, 74]);
682 return true;
683 }
684 _ => return false,
685 }
686
687 self.track_sgr(param);
688 true
689 }
690}
691
692impl TerminalPerformer {
694 fn handle_sgr_dispatch(&mut self, params: &vte::Params) {
696 self.handle_sgr_params(params);
698
699 let param_arrays: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
701
702 if param_arrays.len() >= 3 {
703 let mut i = 0;
704 while i < param_arrays.len() {
705 if param_arrays[i].len() == 1 {
706 if param_arrays[i][0] == 38 && i + 2 < param_arrays.len() {
707 if param_arrays[i + 1].len() == 1
709 && param_arrays[i + 1][0] == 5
710 && param_arrays[i + 2].len() == 1
711 {
712 let color = param_arrays[i + 2][0] as u8;
714 self.attributes.fg_color = Some(TerminalColor::Color256(color));
715 i += 3;
716 continue;
717 } else if param_arrays[i + 1].len() == 1
718 && param_arrays[i + 1][0] == 2
719 && i + 4 < param_arrays.len()
720 && param_arrays[i + 2].len() == 1
721 && param_arrays[i + 3].len() == 1
722 && param_arrays[i + 4].len() == 1
723 {
724 let r = param_arrays[i + 2][0] as u8;
726 let g = param_arrays[i + 3][0] as u8;
727 let b = param_arrays[i + 4][0] as u8;
728 self.attributes.fg_color = Some(TerminalColor::TrueColor { r, g, b });
729 i += 5;
730 continue;
731 }
732 } else if param_arrays[i][0] == 48 && i + 2 < param_arrays.len() {
733 if param_arrays[i + 1].len() == 1
735 && param_arrays[i + 1][0] == 5
736 && param_arrays[i + 2].len() == 1
737 {
738 let color = param_arrays[i + 2][0] as u8;
740 self.attributes.bg_color = Some(TerminalColor::Color256(color));
741 i += 3;
742 continue;
743 } else if param_arrays[i + 1].len() == 1
744 && param_arrays[i + 1][0] == 2
745 && i + 4 < param_arrays.len()
746 && param_arrays[i + 2].len() == 1
747 && param_arrays[i + 3].len() == 1
748 && param_arrays[i + 4].len() == 1
749 {
750 let r = param_arrays[i + 2][0] as u8;
752 let g = param_arrays[i + 3][0] as u8;
753 let b = param_arrays[i + 4][0] as u8;
754 self.attributes.bg_color = Some(TerminalColor::TrueColor { r, g, b });
755 i += 5;
756 continue;
757 }
758 }
759 }
760 i += 1;
761 }
762 }
763 }
764
765 fn handle_osc_params(&mut self, params: &[&[u8]], _bell_terminated: bool) {
767 if params.is_empty() {
768 return;
769 }
770
771 let param_strings: Vec<String> =
773 params.iter().map(|p| String::from_utf8_lossy(p).to_string()).collect();
774
775 if param_strings.is_empty() {
776 return;
777 }
778
779 if param_strings[0] == "8" && param_strings.len() >= 3 {
781 let params =
786 if param_strings.len() > 1 { param_strings[1].clone() } else { String::new() };
787
788 let url =
789 if param_strings.len() > 2 { param_strings[2].clone() } else { String::new() };
790
791 let mut hyperlink_id = None;
793 for param in params.split(':') {
794 let parts: Vec<&str> = param.split('=').collect();
795 if parts.len() == 2 && parts[0] == "id" {
796 hyperlink_id = Some(parts[1].to_string());
797 }
798 }
799
800 if url.is_empty() {
802 self.reset_hyperlink();
804 } else {
805 self.attributes.style.set(CellStyle::HYPERLINK, true);
807 self.attributes.hyperlink_url = Some(url.clone());
808 self.current_hyperlink_url = Some(url);
809
810 if let Some(id) = hyperlink_id {
811 self.current_hyperlink_id = Some(id);
812 }
813 }
814 }
815 }
817
818 fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 {
819 params.iter().nth(index).and_then(|p| p.first().copied()).unwrap_or(default)
820 }
821
822 fn handle_cursor_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
823 let n = usize::from(Self::csi_param(params, 0, 1));
824 let current_row = screen.cursor_row();
825 let current_col = screen.cursor_col();
826
827 match c {
828 'A' => screen.move_cursor(current_row.saturating_sub(n), current_col),
829 'B' => screen.move_cursor(current_row + n, current_col),
830 'C' => screen.move_cursor(current_row, current_col + n),
831 'D' => screen.move_cursor(current_row, current_col.saturating_sub(n)),
832 'H' | 'f' => {
833 let row = usize::from(Self::csi_param(params, 0, 1)).saturating_sub(1);
834 let col = usize::from(Self::csi_param(params, 1, 1)).saturating_sub(1);
835 screen.move_cursor(row, col);
836 }
837 _ => return false,
838 }
839
840 true
841 }
842
843 fn clear_line_to_cursor(screen: &mut Screen) {
844 let row = screen.cursor_row();
845 let col = screen.cursor_col();
846
847 if row < screen.lines.len() {
848 for i in 0..=col.min(screen.lines[row].len().saturating_sub(1)) {
849 screen.lines[row][i] = ScreenCell::default();
850 }
851 }
852 }
853
854 fn handle_erase_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
855 match c {
856 'J' => Self::handle_erase_display(screen, Self::csi_param(params, 0, 0)),
857 'K' => Self::handle_erase_line(screen, Self::csi_param(params, 0, 0)),
858 _ => return false,
859 }
860
861 true
862 }
863
864 fn handle_erase_display(screen: &mut Screen, mode: u16) {
865 match mode {
866 0 => {
867 screen.clear_line_forward();
868 let row = screen.cursor_row();
869 if row + 1 < screen.lines.len() {
870 for i in row + 1..screen.lines.len() {
871 screen.lines[i] = vec![ScreenCell::default(); screen.columns];
872 }
873 }
874 }
875 1 => {
876 Self::clear_line_to_cursor(screen);
877 for i in 0..screen.cursor_row() {
878 if i < screen.lines.len() {
879 screen.lines[i] = vec![ScreenCell::default(); screen.columns];
880 }
881 }
882 }
883 2 | 3 => screen.clear(),
884 _ => debug!("Unhandled erase in display: {}", mode),
885 }
886 }
887
888 fn handle_erase_line(screen: &mut Screen, mode: u16) {
889 match mode {
890 0 => screen.clear_line_forward(),
891 1 => Self::clear_line_to_cursor(screen),
892 2 => screen.clear_line(),
893 _ => debug!("Unhandled erase in line: {}", mode),
894 }
895 }
896
897 fn handle_scroll_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
898 let n = usize::from(Self::csi_param(params, 0, 1));
899
900 match c {
901 'S' => {
902 for _ in 0..n {
903 screen.scroll_up();
904 }
905 }
906 'T' => {
907 let columns = screen.columns;
908 for _ in 0..n {
909 screen.lines.push_front(vec![ScreenCell::default(); columns]);
910 if screen.lines.len() > screen.max_lines {
911 screen.lines.pop_back();
912 }
913 }
914 screen.move_cursor(screen.cursor_row() + n, screen.cursor_col());
915 }
916 _ => return false,
917 }
918
919 true
920 }
921}
922
923impl Perform for TerminalPerformer {
925 fn print(&mut self, c: char) {
926 if let Ok(mut screen) = self.screen.lock() {
927 screen.put_char(c, self.attributes.clone());
928 } else {
929 warn!("Failed to lock screen for print");
930 }
931 }
932
933 fn execute(&mut self, byte: u8) {
934 if let Ok(mut screen) = self.screen.lock() {
935 match byte {
936 b'\r' => screen.carriage_return(),
937 b'\n' => {
938 screen.carriage_return();
939 screen.linefeed();
940 }
941 b'\t' => {
942 let current_col = screen.cursor_col();
944 let new_col = (current_col + 8) & !7;
945 let current_row = screen.cursor_row();
947 screen.move_cursor(current_row, new_col);
948 }
949 b'\x08' => {
950 if screen.cursor_col() > 0 {
952 let current_row = screen.cursor_row();
953 let new_col = screen.cursor_col() - 1;
954 screen.move_cursor(current_row, new_col);
955 }
956 }
957 b'\x0C' => {
958 screen.clear();
960 }
961 b'\x07' => { }
963 _ => {
964 debug!("Unhandled execute: {:?}", byte);
965 }
966 }
967 } else {
968 warn!("Failed to lock screen for execute");
969 }
970 }
971
972 fn hook(&mut self, _params: &vte::Params, _intermediates: &[u8], _ignore: bool, _c: char) {
973 }
975
976 fn put(&mut self, _byte: u8) {
977 }
979
980 fn unhook(&mut self) {
981 }
983
984 fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
985 self.handle_osc_params(params, bell_terminated);
987 }
988
989 fn csi_dispatch(
990 &mut self,
991 params: &vte::Params,
992 _intermediates: &[u8],
993 _ignore: bool,
994 c: char,
995 ) {
996 if c == 'm' {
998 self.handle_sgr_dispatch(params);
999 return;
1000 }
1001
1002 if let Ok(mut screen) = self.screen.lock() {
1003 if Self::handle_cursor_csi(&mut screen, params, c)
1004 || Self::handle_erase_csi(&mut screen, params, c)
1005 || Self::handle_scroll_csi(&mut screen, params, c)
1006 {
1007 return;
1008 }
1009
1010 debug!("Unhandled CSI: {:?} {:?}", params, c);
1011 } else {
1012 warn!("Failed to lock screen for csi_dispatch");
1013 }
1014 }
1015
1016 fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
1017 if intermediates.is_empty() {
1018 match byte {
1019 b'c' => {
1020 if let Ok(mut screen) = self.screen.lock() {
1022 screen.clear();
1023 }
1024 self.reset_attributes();
1025 }
1026 b'7' | b'8' => {
1027 }
1030 _ => debug!("Unhandled ESC dispatch: {:?}", byte),
1031 }
1032 }
1033 }
1034}
1035
1036#[derive(Clone)]
1038pub struct TerminalEmulator {
1039 performer: TerminalPerformer,
1041 screen: Arc<Mutex<Screen>>,
1043}
1044
1045impl std::fmt::Debug for TerminalEmulator {
1047 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1048 f.debug_struct("TerminalEmulator")
1049 .field("performer", &self.performer)
1050 .finish_non_exhaustive()
1051 }
1052}
1053
1054impl TerminalEmulator {
1055 pub fn new(columns: usize) -> Self {
1057 let screen = Arc::new(Mutex::new(Screen::new(columns)));
1058 let performer = TerminalPerformer::new(screen.clone());
1059
1060 Self { performer, screen }
1061 }
1062
1063 pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
1065 let screen = Arc::new(Mutex::new(Screen::new_with_max_lines(columns, max_lines)));
1066 let performer = TerminalPerformer::new(screen.clone());
1067
1068 Self { performer, screen }
1069 }
1070
1071 pub fn process(&mut self, data: &str) {
1073 let mut parser = Parser::new();
1074
1075 let chunk_size = 4096;
1077 let data_bytes = data.as_bytes();
1078
1079 for chunk in data_bytes.chunks(chunk_size) {
1080 parser.advance(&mut self.performer, chunk);
1081 }
1082 }
1083
1084 pub fn process_with_limited_buffer(&mut self, data: &str, max_lines: usize) {
1086 if let Ok(mut screen) = self.screen.lock() {
1087 screen.max_lines = max_lines.min(MAX_SCREEN_LINES);
1089 }
1090
1091 self.process(data);
1092
1093 if let Ok(mut screen) = self.screen.lock() {
1095 if screen.lines.len() > max_lines {
1096 screen.smart_truncate(max_lines);
1097 }
1098 }
1099 }
1100
1101 pub fn get_screen(&self) -> Arc<Mutex<Screen>> {
1103 self.screen.clone()
1104 }
1105
1106 pub fn display(&self) -> Vec<String> {
1108 if let Ok(screen) = self.screen.lock() {
1109 screen.display()
1110 } else {
1111 warn!("Failed to lock screen for display");
1112 vec![]
1113 }
1114 }
1115
1116 pub fn to_plain_text(&self) -> String {
1118 if let Ok(screen) = self.screen.lock() {
1119 screen.to_plain_text()
1120 } else {
1121 warn!("Failed to lock screen for to_plain_text");
1122 String::new()
1123 }
1124 }
1125
1126 pub fn clear(&mut self) {
1128 if let Ok(mut screen) = self.screen.lock() {
1129 screen.clear();
1130 } else {
1131 warn!("Failed to lock screen for clear");
1132 }
1133 }
1134}
1135
1136type CacheEntryMap = HashMap<u64, (Vec<String>, Instant)>;
1143
1144fn hash_terminal_text(text: &str) -> u64 {
1146 use std::hash::{Hash, Hasher};
1147 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1148 text.hash(&mut hasher);
1149 hasher.finish()
1150}
1151
1152#[derive(Debug, Default)]
1155struct CacheInner {
1156 map: CacheEntryMap,
1157 order: VecDeque<u64>,
1159}
1160
1161#[derive(Debug, Clone)]
1163struct TerminalCache {
1164 inner: Arc<RwLock<CacheInner>>,
1165 max_entries: usize,
1167 ttl: u64,
1169}
1170
1171impl TerminalCache {
1172 fn new(max_entries: usize, ttl: u64) -> Self {
1174 Self { inner: Arc::new(RwLock::new(CacheInner::default())), max_entries, ttl }
1175 }
1176
1177 fn get(&self, text: &str) -> Option<Vec<String>> {
1179 let key = hash_terminal_text(text);
1180 let inner = self.inner.read().ok()?;
1181 let (value, timestamp) = inner.map.get(&key)?;
1182 if timestamp.elapsed().as_secs() < self.ttl {
1183 Some(value.clone())
1184 } else {
1185 None
1186 }
1187 }
1188
1189 fn insert(&self, text: &str, value: Vec<String>) {
1191 let key = hash_terminal_text(text);
1192 let Ok(mut inner) = self.inner.write() else {
1193 return;
1194 };
1195 if inner.map.insert(key, (value, Instant::now())).is_none() {
1196 inner.order.push_back(key);
1197 }
1198 while inner.order.len() > self.max_entries {
1199 let Some(old) = inner.order.pop_front() else {
1200 break;
1201 };
1202 inner.map.remove(&old);
1203 }
1204 }
1205
1206 fn cleanup(&self) {
1208 let Ok(mut inner) = self.inner.write() else {
1209 return;
1210 };
1211 let ttl = self.ttl;
1212 let CacheInner { map, order } = &mut *inner;
1213 map.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < ttl);
1214 order.retain(|key| map.contains_key(key));
1215 }
1216}
1217
1218lazy_static::lazy_static! {
1220 static ref TERMINAL_CACHE: TerminalCache = TerminalCache::new(100, CACHE_TTL);
1221}
1222
1223#[derive(Debug, Clone)]
1225pub struct TerminalOutputDiff {
1226 previous_output: Vec<String>,
1228 output_hash: String,
1230 max_lines: usize,
1232}
1233
1234impl Default for TerminalOutputDiff {
1235 fn default() -> Self {
1236 Self::new()
1237 }
1238}
1239
1240impl TerminalOutputDiff {
1241 pub fn new() -> Self {
1243 Self { previous_output: Vec::new(), output_hash: String::new(), max_lines: 1000 }
1244 }
1245
1246 pub fn new_with_max_lines(max_lines: usize) -> Self {
1248 Self { previous_output: Vec::new(), output_hash: String::new(), max_lines }
1249 }
1250
1251 pub fn detect_changes(&mut self, new_output: &[String]) -> Vec<String> {
1253 if self.previous_output.is_empty() {
1254 self.previous_output = new_output.to_vec();
1256 self.output_hash = self.calculate_hash(new_output);
1257 return new_output.to_vec();
1258 }
1259
1260 let new_hash = self.calculate_hash(new_output);
1262 if new_hash == self.output_hash {
1263 return Vec::new(); }
1265
1266 let mut changes = Vec::new();
1268
1269 let nold = self.previous_output.len().min(self.max_lines);
1271 let nnew = new_output.len().min(self.max_lines);
1272
1273 let mut matched_position = None;
1275
1276 let is_prefix = nold <= nnew && (0..nold).all(|i| self.previous_output[i] == new_output[i]);
1278
1279 if is_prefix {
1280 matched_position = Some(nold);
1282 } else {
1283 let mut best_match = 0;
1285 let mut best_position = 0;
1286
1287 let window_size = 3.min(nold); if window_size > 0 {
1291 for i in (0..=nnew.saturating_sub(window_size)).rev() {
1292 let mut match_count = 0;
1294 for j in 0..window_size {
1295 if i + j < nnew
1296 && nold.saturating_sub(window_size) + j < nold
1297 && new_output[i + j]
1298 == self.previous_output[nold.saturating_sub(window_size) + j]
1299 {
1300 match_count += 1;
1301 }
1302 }
1303
1304 if match_count > best_match {
1305 best_match = match_count;
1306 best_position = i + window_size;
1307
1308 if best_match == window_size {
1309 break;
1311 }
1312 }
1313 }
1314 }
1315
1316 if best_match >= window_size / 2 {
1317 matched_position = Some(best_position);
1319 }
1320 }
1321
1322 if let Some(pos) = matched_position {
1324 if pos < nnew {
1325 changes = new_output[pos..].to_vec();
1326
1327 if !changes.is_empty()
1329 && !self.previous_output.is_empty()
1330 && changes[0] == self.previous_output[self.previous_output.len() - 1]
1331 {
1332 changes.remove(0);
1333 }
1334 }
1335 } else {
1336 changes = new_output.to_vec();
1338 }
1339
1340 self.previous_output = new_output.to_vec();
1342 self.output_hash = new_hash;
1343
1344 changes
1345 }
1346
1347 fn calculate_hash(&self, lines: &[String]) -> String {
1349 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1352 for line in lines.iter().take(self.max_lines) {
1353 std::hash::Hash::hash(line, &mut hasher);
1354 }
1355 format!("{:x}", std::hash::Hasher::finish(&hasher))
1356 }
1357
1358 pub fn reset(&mut self) {
1360 self.previous_output.clear();
1361 self.output_hash.clear();
1362 }
1363}
1364
1365pub fn render_terminal_output(text: &str) -> Vec<String> {
1367 if let Some(cached) = TERMINAL_CACHE.get(text) {
1369 return cached;
1370 }
1371
1372 let mut terminal = TerminalEmulator::new(DEFAULT_COLUMNS);
1373
1374 if text.len() > MAX_OUTPUT_SIZE {
1376 terminal.process_with_limited_buffer(text, DEFAULT_MAX_SCREEN_LINES);
1378 } else {
1379 terminal.process(text);
1380 }
1381
1382 let result = terminal.display();
1383
1384 if text.len() < MAX_OUTPUT_SIZE {
1386 TERMINAL_CACHE.insert(text, result.clone());
1387 }
1388
1389 if rand::random::<u32>() % 100 == 0 {
1391 TERMINAL_CACHE.cleanup();
1392 }
1393
1394 result.into_iter().map(|line| strip_ansi_codes(&line)).collect()
1397}
1398
1399pub fn incremental_text(text: &str, last_pending_output: &str) -> String {
1401 if text.is_empty() {
1403 return String::new();
1404 }
1405
1406 if last_pending_output.is_empty() {
1408 let lines = render_terminal_output(text);
1410 return lines.join("\n").trim().to_string();
1411 }
1412
1413 let is_append = text.starts_with(last_pending_output);
1415
1416 if is_append && text.len() > last_pending_output.len() {
1417 let new_part = &text[last_pending_output.len()..];
1419
1420 let context_len = 200.min(last_pending_output.len());
1422 let full_context = if context_len > 0 {
1423 let start_pos = last_pending_output.len() - context_len;
1424 format!("{}{}", &last_pending_output[start_pos..], new_part)
1425 } else {
1426 new_part.to_string()
1427 };
1428
1429 let previous_lines = render_terminal_output(last_pending_output);
1431 let combined_lines = render_terminal_output(&full_context);
1432
1433 let mut diff_detector = TerminalOutputDiff::new();
1435 diff_detector.previous_output = previous_lines;
1436
1437 let changes = diff_detector.detect_changes(&combined_lines);
1439
1440 if changes.is_empty() {
1441 return String::new();
1442 }
1443
1444 return changes.join("\n");
1445 }
1446
1447 let text_limit = if text.len() > MAX_OUTPUT_SIZE {
1451 let start_offset = text.len() - MAX_OUTPUT_SIZE;
1452
1453 let adjusted_offset =
1455 text[start_offset..].find('\n').map_or(start_offset, |pos| start_offset + pos + 1);
1456
1457 &text[adjusted_offset..]
1458 } else {
1459 text
1460 };
1461
1462 let previous_lines = render_terminal_output(last_pending_output);
1464 let new_lines = render_terminal_output(text_limit);
1465
1466 let mut diff_detector = TerminalOutputDiff::new();
1468 diff_detector.previous_output = previous_lines;
1469
1470 let changes = diff_detector.detect_changes(&new_lines);
1472
1473 if changes.is_empty() {
1474 return String::new();
1475 }
1476
1477 changes.join("\n")
1478}
1479
1480pub fn strip_ansi_codes(input: &str) -> String {
1482 static RE: std::sync::OnceLock<Option<Regex>> = std::sync::OnceLock::new();
1483
1484 if !input.contains('\u{1b}') {
1486 return input.to_string();
1487 }
1488
1489 let re = RE.get_or_init(|| {
1499 Regex::new(
1500 r"\x1b\[[0-9;:?<>=!]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[=>MNcD78]",
1501 )
1502 .ok()
1503 });
1504 let cleaned = match re {
1505 Some(re) => re.replace_all(input, "").into_owned(),
1506 None => input.to_string(),
1507 };
1508 cleaned.replace('\u{1b}', "")
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514 use super::*;
1515
1516 #[test]
1517 fn render_applies_cursor_left_overwrite() {
1518 let rendered = render_terminal_output("ZZZZZZZZZZ\u{1b}[5DQQQQQ").join("\n");
1521 assert!(rendered.contains("ZZZZZQQQQQ"), "cursor-left not applied; got {rendered:?}");
1522 assert!(
1524 !rendered.contains("ZZZZZZZZZZQQQQQ"),
1525 "looks like a plain strip; got {rendered:?}"
1526 );
1527 }
1528
1529 #[test]
1530 fn strips_csi_osc_and_cursor_sequences() {
1531 assert_eq!(strip_ansi_codes("\u{1b}[1;35mhi\u{1b}[0m"), "hi");
1533 assert_eq!(strip_ansi_codes("\u{1b}[?2004h>>> \u{1b}[?2004l\u{1b}[4D42"), ">>> 42");
1535 assert_eq!(strip_ansi_codes("\u{1b}]0;user@host\u{7}prompt$ "), "prompt$ ");
1537 assert_eq!(strip_ansi_codes("no escapes here"), "no escapes here");
1539 }
1540
1541 #[test]
1542 fn test_screen_basic_operations() {
1543 let mut screen = Screen::new(80);
1544
1545 let _attributes = ScreenCellAttributes::default();
1547
1548 screen.put_char_basic('H', CellStyle::default(), None, None);
1550 screen.put_char_basic('e', CellStyle::default(), None, None);
1551 screen.put_char_basic('l', CellStyle::default(), None, None);
1552 screen.put_char_basic('l', CellStyle::default(), None, None);
1553 screen.put_char_basic('o', CellStyle::default(), None, None);
1554
1555 let display = screen.display();
1556 assert_eq!(display, vec!["Hello"]);
1557
1558 screen.carriage_return();
1560 screen.linefeed();
1561
1562 screen.put_char_basic('W', CellStyle::default(), None, None);
1563 screen.put_char_basic('o', CellStyle::default(), None, None);
1564 screen.put_char_basic('r', CellStyle::default(), None, None);
1565 screen.put_char_basic('l', CellStyle::default(), None, None);
1566 screen.put_char_basic('d', CellStyle::default(), None, None);
1567
1568 let display = screen.display();
1569 assert_eq!(display, vec!["Hello", "World"]);
1570
1571 screen.clear_line();
1573 let display = screen.display();
1574 assert_eq!(display, vec!["Hello"]);
1575 }
1576
1577 #[test]
1578 fn test_terminal_emulator_basic() {
1579 let mut terminal = TerminalEmulator::new(80);
1580
1581 terminal.process("Hello\r\nWorld");
1583 let display = terminal.display();
1584 assert_eq!(display, vec!["Hello", "World"]);
1585
1586 terminal.clear();
1588 terminal.process("Normal \x1b[1mBold\x1b[0m Normal");
1589 let display = terminal.display();
1590 assert_eq!(display, vec!["Normal Bold Normal"]);
1591
1592 terminal.clear();
1594 terminal.process("Hello\x1b[5D_\x1b[1C_\x1b[1C_");
1595 let display = terminal.display();
1596 assert_eq!(display, vec!["_e_l_"]);
1597 }
1598
1599 #[test]
1600 fn test_incremental_output() {
1601 let old = vec!["Line 1".to_string(), "Line 2".to_string()];
1602 let new = vec!["Line 1".to_string(), "Line 2".to_string(), "Line 3".to_string()];
1603
1604 let mut diff_detector = TerminalOutputDiff::new();
1605 diff_detector.previous_output = old;
1606
1607 let incremental = diff_detector.detect_changes(&new);
1608 assert_eq!(incremental, vec!["Line 3"]);
1609
1610 let old = vec!["Line A".to_string(), "Line B".to_string()];
1612 let new = vec!["Line X".to_string(), "Line Y".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 X", "Line Y"]);
1619 }
1620
1621 #[test]
1622 fn test_render_terminal_output() {
1623 let text = "Hello\r\nWorld\r\n\x1b[31mRed\x1b[0m Text";
1624 let lines = render_terminal_output(text);
1625 assert_eq!(lines, vec!["Hello", "World", "Red Text"]);
1626 }
1627
1628 #[test]
1629 fn test_smart_truncate() {
1630 let mut screen = Screen::new_with_max_lines(80, 20);
1631
1632 for i in 0..30 {
1634 let line = format!("Line {i}");
1635 for c in line.chars() {
1636 screen.put_char(c, ScreenCellAttributes::default());
1637 }
1638 screen.carriage_return();
1639 screen.linefeed();
1640 }
1641
1642 assert_eq!(screen.lines.len(), 20);
1644
1645 screen.smart_truncate(10);
1647
1648 assert_eq!(screen.lines.len(), 10);
1650
1651 let has_truncation_marker = screen.lines.iter().any(|line| {
1653 let line_text: String = line.iter().map(|cell| cell.character).collect();
1654 line_text.contains("TRUNCATED")
1655 });
1656
1657 assert!(has_truncation_marker);
1658 }
1659
1660 #[test]
1661 fn test_terminal_cache() {
1662 let cache = TerminalCache::new(10, 60);
1663
1664 cache.insert("test", vec!["line1".to_string(), "line2".to_string()]);
1666
1667 let retrieved = cache.get("test");
1669 assert_eq!(retrieved, Some(vec!["line1".to_string(), "line2".to_string()]));
1670
1671 let not_found = cache.get("unknown");
1673 assert_eq!(not_found, None);
1674 }
1675
1676 #[test]
1677 fn test_incremental_text_append() {
1678 let old_text = "Line 1\nLine 2\n";
1679 let new_text = "Line 1\nLine 2\nLine 3\n";
1680
1681 let incremental = incremental_text(new_text, old_text);
1682 assert_eq!(incremental, "Line 3");
1683 }
1684
1685 #[test]
1686 fn test_terminal_color_handling() {
1687 let mut terminal = TerminalEmulator::new(80);
1688
1689 terminal.process("\x1b[31mRed\x1b[32mGreen\x1b[0mNormal");
1691 let display = terminal.display();
1692 assert_eq!(display, vec!["RedGreenNormal"]);
1693
1694 terminal.clear();
1696 terminal.process("\x1b[38;5;208mOrange\x1b[0mNormal");
1697 let display = terminal.display();
1698 assert_eq!(display, vec!["OrangeNormal"]);
1699 }
1700}