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 = 160;
18pub const MAX_OUTPUT_SIZE: usize = 500_000;
20const CACHE_TTL: u64 = 300; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub struct CellStyle(u32);
26
27impl CellStyle {
28 pub const BOLD: Self = Self(1 << 0);
29 pub const UNDERLINE: Self = Self(1 << 1);
30 pub const BLINK: Self = Self(1 << 2);
31 pub const REVERSE: Self = Self(1 << 3);
32 pub const ITALIC: Self = Self(1 << 4);
33 pub const STRIKETHROUGH: Self = Self(1 << 5);
34 pub const DIM: Self = Self(1 << 6);
35 pub const DOUBLE_UNDERLINE: Self = Self(1 << 7);
36 pub const FRAMED: Self = Self(1 << 8);
37 pub const ENCIRCLED: Self = Self(1 << 9);
38 pub const OVERLINED: Self = Self(1 << 10);
39 pub const FRAKTUR: Self = Self(1 << 11);
40 pub const CONCEAL: Self = Self(1 << 12);
41 pub const SUPERSCRIPT: Self = Self(1 << 13);
42 pub const SUBSCRIPT: Self = Self(1 << 14);
43 pub const HYPERLINK: Self = Self(1 << 15);
44
45 #[must_use]
46 pub const fn union(self, other: Self) -> Self {
47 Self(self.0 | other.0)
48 }
49
50 #[must_use]
51 pub const fn contains(self, flag: Self) -> bool {
52 self.0 & flag.0 != 0
53 }
54
55 pub fn set(&mut self, flag: Self, enabled: bool) {
56 if enabled {
57 self.0 |= flag.0;
58 } else {
59 self.0 &= !flag.0;
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq)]
66pub struct ScreenCellAttributes {
67 pub style: CellStyle,
69 pub fg_color: Option<TerminalColor>,
71 pub bg_color: Option<TerminalColor>,
73 pub hyperlink_url: Option<String>,
75 pub font: u8,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ScreenCell {
82 pub character: char,
84 pub style: CellStyle,
86 pub fg_color: Option<TerminalColor>,
88 pub bg_color: Option<TerminalColor>,
90 pub hyperlink_url: Option<String>,
92 pub font: u8,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum TerminalColor {
99 Basic(u8),
101 Color256(u8),
103 TrueColor { r: u8, g: u8, b: u8 },
105 Named(String),
107}
108
109impl ScreenCell {
110 fn new(character: char, attributes: ScreenCellAttributes) -> Self {
111 Self {
112 character,
113 style: attributes.style,
114 fg_color: attributes.fg_color,
115 bg_color: attributes.bg_color,
116 hyperlink_url: attributes.hyperlink_url,
117 font: attributes.font,
118 }
119 }
120}
121
122impl Default for ScreenCell {
123 fn default() -> Self {
124 Self {
125 character: ' ',
126 style: CellStyle::default(),
127 fg_color: None,
128 bg_color: None,
129 hyperlink_url: None,
130 font: 0, }
132 }
133}
134
135#[derive(Debug, Clone)]
137pub struct Screen {
138 pub lines: VecDeque<Vec<ScreenCell>>,
140 pub cursor_position: (usize, usize),
142 pub columns: usize,
144 pub cursor_visible: bool,
146 pub max_lines: usize,
148 last_modified: Instant,
150}
151
152impl Default for Screen {
153 fn default() -> Self {
154 let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
155 lines.push_back(vec![ScreenCell::default(); DEFAULT_COLUMNS]);
156
157 Self {
158 lines,
159 cursor_position: (0, 0),
160 columns: DEFAULT_COLUMNS,
161 cursor_visible: true,
162 max_lines: DEFAULT_MAX_SCREEN_LINES,
163 last_modified: Instant::now(),
164 }
165 }
166}
167
168impl Screen {
169 pub fn new(columns: usize) -> Self {
171 let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
172 lines.push_back(vec![ScreenCell::default(); columns]);
173
174 Self {
175 lines,
176 cursor_position: (0, 0),
177 columns,
178 cursor_visible: true,
179 max_lines: DEFAULT_MAX_SCREEN_LINES,
180 last_modified: Instant::now(),
181 }
182 }
183
184 pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
186 let mut lines = VecDeque::with_capacity(max_lines.min(MAX_SCREEN_LINES));
187 lines.push_back(vec![ScreenCell::default(); columns]);
188
189 Self {
190 lines,
191 cursor_position: (0, 0),
192 columns,
193 cursor_visible: true,
194 max_lines: max_lines.min(MAX_SCREEN_LINES),
195 last_modified: Instant::now(),
196 }
197 }
198
199 pub fn cursor_row(&self) -> usize {
201 self.cursor_position.0
202 }
203
204 pub fn cursor_col(&self) -> usize {
206 self.cursor_position.1
207 }
208
209 fn ensure_line(&mut self, line_idx: usize) {
211 while self.lines.len() <= line_idx {
213 self.lines.push_back(vec![ScreenCell::default(); self.columns]);
214 }
215
216 while self.lines.len() > self.max_lines {
218 self.lines.pop_front();
219
220 if self.cursor_position.0 > 0 {
222 self.cursor_position.0 -= 1;
223 }
224 }
225
226 self.last_modified = Instant::now();
227 }
228
229 fn ensure_cursor_position(&mut self) {
231 self.ensure_line(self.cursor_position.0);
232
233 if self.cursor_position.1 >= self.columns {
235 self.cursor_position.1 = self.columns - 1;
236 }
237
238 self.last_modified = Instant::now();
239 }
240
241 pub fn put_char(&mut self, c: char, attributes: ScreenCellAttributes) {
243 self.ensure_cursor_position();
244
245 let row = self.cursor_position.0;
247 let col = self.cursor_position.1;
248
249 if col < self.lines[row].len() {
251 self.lines[row][col] = ScreenCell::new(c, attributes);
252 } else {
253 while self.lines[row].len() <= col {
255 self.lines[row].push(ScreenCell::default());
256 }
257 self.lines[row][col] = ScreenCell::new(c, attributes);
258 }
259
260 self.cursor_position.1 += 1;
262 if self.cursor_position.1 >= self.columns {
263 self.cursor_position.1 = 0;
264 self.cursor_position.0 += 1;
265 self.ensure_cursor_position();
266 }
267
268 self.last_modified = Instant::now();
269 }
270
271 #[allow(clippy::too_many_arguments)]
273 pub fn put_char_basic(
274 &mut self,
275 c: char,
276 style: CellStyle,
277 fg_color: Option<TerminalColor>,
278 bg_color: Option<TerminalColor>,
279 ) {
280 let attributes = ScreenCellAttributes { style, fg_color, bg_color, ..Default::default() };
281
282 self.put_char(c, attributes);
283 }
284
285 pub fn move_cursor(&mut self, row: usize, col: usize) {
287 self.cursor_position = (row, col);
288 self.ensure_cursor_position();
289 self.last_modified = Instant::now();
290 }
291
292 pub fn linefeed(&mut self) {
294 self.cursor_position.0 += 1;
295 self.ensure_cursor_position();
296 self.last_modified = Instant::now();
297 }
298
299 pub fn carriage_return(&mut self) {
301 self.cursor_position.1 = 0;
302 self.last_modified = Instant::now();
303 }
304
305 pub fn clear(&mut self) {
307 self.lines.clear();
308 self.lines.push_back(vec![ScreenCell::default(); self.columns]);
309 self.cursor_position = (0, 0);
310 self.last_modified = Instant::now();
311 }
312
313 pub fn clear_line_forward(&mut self) {
315 let row = self.cursor_position.0;
316 let col = self.cursor_position.1;
317
318 if row < self.lines.len() {
319 for i in col..self.lines[row].len() {
320 self.lines[row][i] = ScreenCell::default();
321 }
322 }
323 self.last_modified = Instant::now();
324 }
325
326 pub fn clear_line(&mut self) {
328 let row = self.cursor_position.0;
329 if row < self.lines.len() {
330 self.lines[row] = vec![ScreenCell::default(); self.columns];
331 }
332 self.last_modified = Instant::now();
333 }
334
335 pub fn scroll_up(&mut self) {
337 if !self.lines.is_empty() {
338 self.lines.pop_front();
339 self.ensure_line(self.cursor_position.0);
340 }
341 self.last_modified = Instant::now();
342 }
343
344 pub fn smart_truncate(&mut self, max_size: usize) {
346 let current_size = self.lines.len();
347
348 if current_size <= max_size {
349 return;
350 }
351
352 let to_remove = current_size - max_size;
354
355 let beginning_lines = max_size / 10; if to_remove <= beginning_lines {
359 for _ in 0..to_remove {
361 self.lines.pop_front();
362 }
363 } else {
364 let end_lines = max_size - beginning_lines - 1; let beginning: VecDeque<Vec<ScreenCell>> =
369 self.lines.drain(0..beginning_lines.min(self.lines.len())).collect();
370
371 let end_start_index = self.lines.len().saturating_sub(end_lines);
372 let end: VecDeque<Vec<ScreenCell>> = self.lines.drain(end_start_index..).collect();
373
374 self.lines.clear();
376
377 for line in beginning {
379 self.lines.push_back(line);
380 }
381
382 let mut marker_line = vec![ScreenCell::default(); self.columns];
384 let marker_text = " [... TRUNCATED OUTPUT ...] ";
385
386 for (i, c) in marker_text.chars().enumerate() {
387 if i < self.columns {
388 marker_line[i] = ScreenCell {
389 character: c,
390 style: CellStyle::BOLD.union(CellStyle::REVERSE),
391 ..ScreenCell::default()
392 };
393 }
394 }
395
396 self.lines.push_back(marker_line);
397
398 for line in end {
400 self.lines.push_back(line);
401 }
402 }
403
404 if self.cursor_position.0 >= self.lines.len() {
406 self.cursor_position.0 = self.lines.len().saturating_sub(1);
407 }
408
409 self.last_modified = Instant::now();
410 }
411
412 pub fn to_plain_text(&self) -> String {
414 let mut result = String::with_capacity(self.lines.len() * self.columns);
415
416 for line in &self.lines {
417 let line_text: String = line.iter().map(|cell| cell.character).collect();
418 result.push_str(&line_text);
419 result.push('\n');
420 }
421
422 result
423 }
424
425 pub fn display(&self) -> Vec<String> {
427 let mut result = Vec::with_capacity(self.lines.len());
428
429 for line in &self.lines {
430 let line_text: String = line.iter().map(|cell| cell.character).collect();
431
432 let trimmed = line_text.trim_end();
434 result.push(trimmed.to_string());
435 }
436
437 while let Some(last) = result.last() {
439 if last.is_empty() {
440 result.pop();
441 } else {
442 break;
443 }
444 }
445
446 result
447 }
448
449 pub fn last_modified(&self) -> Instant {
451 self.last_modified
452 }
453
454 pub fn time_since_last_modified(&self) -> f64 {
456 self.last_modified.elapsed().as_secs_f64()
457 }
458}
459
460#[derive(Clone)]
462pub struct TerminalPerformer {
463 screen: Arc<Mutex<Screen>>,
465 attributes: ScreenCellAttributes,
467 sgr_state: HashMap<u16, bool>,
469 current_hyperlink_id: Option<String>,
471 current_hyperlink_url: Option<String>,
473 osc_params: Vec<String>,
475}
476
477impl std::fmt::Debug for TerminalPerformer {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 f.debug_struct("TerminalPerformer")
481 .field("attributes", &self.attributes)
482 .field("hyperlink_id", &self.current_hyperlink_id)
483 .field("hyperlink_url", &self.current_hyperlink_url)
484 .finish_non_exhaustive()
485 }
486}
487
488impl TerminalPerformer {
489 pub fn new(screen: Arc<Mutex<Screen>>) -> Self {
491 Self {
492 screen,
493 attributes: ScreenCellAttributes::default(),
494 sgr_state: HashMap::new(),
495 current_hyperlink_id: None,
496 current_hyperlink_url: None,
497 osc_params: Vec::new(),
498 }
499 }
500
501 pub fn screen(&self) -> &Arc<Mutex<Screen>> {
503 &self.screen
504 }
505
506 fn reset_attributes(&mut self) {
508 self.attributes = ScreenCellAttributes::default();
509 self.sgr_state.clear();
510 }
511
512 fn reset_hyperlink(&mut self) {
514 self.current_hyperlink_id = None;
515 self.current_hyperlink_url = None;
516 self.attributes.style.set(CellStyle::HYPERLINK, false);
517 self.attributes.hyperlink_url = None;
518 }
519
520 fn track_sgr(&mut self, param: u16) {
521 self.sgr_state.insert(param, true);
522 }
523
524 fn untrack_sgr(&mut self, params: &[u16]) {
525 for param in params {
526 self.sgr_state.remove(param);
527 }
528 }
529
530 fn handle_sgr_params(&mut self, params: &vte::Params) {
532 if params.is_empty() {
533 self.reset_attributes();
534 return;
535 }
536
537 for param_values in params.iter().flatten() {
538 self.handle_sgr_param(*param_values);
539 }
540 }
541
542 fn handle_sgr_param(&mut self, param: u16) {
543 if self.handle_basic_sgr_style(param)
544 || self.handle_font_sgr(param)
545 || self.handle_color_sgr(param)
546 || self.handle_frame_sgr(param)
547 || self.handle_script_sgr(param)
548 {
549 return;
550 }
551
552 debug!("Unsupported SGR parameter: {}", param);
553 }
554
555 fn handle_basic_sgr_style(&mut self, param: u16) -> bool {
556 match param {
557 0 => self.reset_attributes(),
558 1 => self.attributes.style.set(CellStyle::BOLD, true),
559 2 => self.attributes.style.set(CellStyle::DIM, true),
560 3 => self.attributes.style.set(CellStyle::ITALIC, true),
561 4 => {
562 self.attributes.style.set(CellStyle::UNDERLINE, true);
563 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
564 }
565 5 | 6 => self.attributes.style.set(CellStyle::BLINK, true),
566 7 => self.attributes.style.set(CellStyle::REVERSE, true),
567 8 => self.attributes.style.set(CellStyle::CONCEAL, true),
568 9 => self.attributes.style.set(CellStyle::STRIKETHROUGH, true),
569 20 => self.attributes.style.set(CellStyle::FRAKTUR, true),
570 21 => {
571 self.attributes.style.set(CellStyle::UNDERLINE, true);
572 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, true);
573 }
574 22 => {
575 self.attributes.style.set(CellStyle::BOLD, false);
576 self.attributes.style.set(CellStyle::DIM, false);
577 self.untrack_sgr(&[1, 2]);
578 return true;
579 }
580 23 => {
581 self.attributes.style.set(CellStyle::ITALIC, false);
582 self.attributes.style.set(CellStyle::FRAKTUR, false);
583 self.untrack_sgr(&[3, 20]);
584 return true;
585 }
586 24 => {
587 self.attributes.style.set(CellStyle::UNDERLINE, false);
588 self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
589 self.untrack_sgr(&[4, 21]);
590 return true;
591 }
592 25 => {
593 self.attributes.style.set(CellStyle::BLINK, false);
594 self.untrack_sgr(&[5, 6]);
595 return true;
596 }
597 27 => self.attributes.style.set(CellStyle::REVERSE, false),
598 28 => self.attributes.style.set(CellStyle::CONCEAL, false),
599 29 => self.attributes.style.set(CellStyle::STRIKETHROUGH, false),
600 _ => return false,
601 }
602
603 self.track_sgr(param);
604 true
605 }
606
607 fn handle_font_sgr(&mut self, param: u16) -> bool {
608 match param {
609 10 => self.attributes.font = 0,
610 11..=19 => self.attributes.font = (param - 10) as u8,
611 _ => return false,
612 }
613
614 self.track_sgr(param);
615 true
616 }
617
618 fn handle_color_sgr(&mut self, param: u16) -> bool {
619 match param {
620 26 | 38 | 48 => {}
621 30..=37 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 30)),
622 39 => self.attributes.fg_color = None,
623 40..=47 => self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 40)),
624 49 => self.attributes.bg_color = None,
625 90..=97 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 90 + 8)),
626 100..=107 => {
627 self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 100 + 8));
628 }
629 _ => return false,
630 }
631
632 true
633 }
634
635 fn handle_frame_sgr(&mut self, param: u16) -> bool {
636 match param {
637 51 => {
638 self.attributes.style.set(CellStyle::FRAMED, true);
639 self.attributes.style.set(CellStyle::ENCIRCLED, false);
640 }
641 52 => {
642 self.attributes.style.set(CellStyle::FRAMED, false);
643 self.attributes.style.set(CellStyle::ENCIRCLED, true);
644 }
645 53 => self.attributes.style.set(CellStyle::OVERLINED, true),
646 54 => {
647 self.attributes.style.set(CellStyle::FRAMED, false);
648 self.attributes.style.set(CellStyle::ENCIRCLED, false);
649 self.untrack_sgr(&[51, 52]);
650 return true;
651 }
652 55 => {
653 self.attributes.style.set(CellStyle::OVERLINED, false);
654 self.untrack_sgr(&[53]);
655 return true;
656 }
657 60..=65 => {}
658 _ => return false,
659 }
660
661 self.track_sgr(param);
662 true
663 }
664
665 fn handle_script_sgr(&mut self, param: u16) -> bool {
666 match param {
667 73 => {
668 self.attributes.style.set(CellStyle::SUPERSCRIPT, true);
669 self.attributes.style.set(CellStyle::SUBSCRIPT, false);
670 }
671 74 => {
672 self.attributes.style.set(CellStyle::SUBSCRIPT, true);
673 self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
674 }
675 75 => {
676 self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
677 self.attributes.style.set(CellStyle::SUBSCRIPT, false);
678 self.untrack_sgr(&[73, 74]);
679 return true;
680 }
681 _ => return false,
682 }
683
684 self.track_sgr(param);
685 true
686 }
687}
688
689impl TerminalPerformer {
691 fn handle_sgr_dispatch(&mut self, params: &vte::Params) {
693 self.handle_sgr_params(params);
695
696 let param_arrays: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
698
699 if param_arrays.len() >= 3 {
700 let mut i = 0;
701 while i < param_arrays.len() {
702 if param_arrays[i].len() == 1 {
703 if param_arrays[i][0] == 38 && i + 2 < param_arrays.len() {
704 if param_arrays[i + 1].len() == 1
706 && param_arrays[i + 1][0] == 5
707 && param_arrays[i + 2].len() == 1
708 {
709 let color = param_arrays[i + 2][0] as u8;
711 self.attributes.fg_color = Some(TerminalColor::Color256(color));
712 i += 3;
713 continue;
714 } else if param_arrays[i + 1].len() == 1
715 && param_arrays[i + 1][0] == 2
716 && i + 4 < param_arrays.len()
717 && param_arrays[i + 2].len() == 1
718 && param_arrays[i + 3].len() == 1
719 && param_arrays[i + 4].len() == 1
720 {
721 let r = param_arrays[i + 2][0] as u8;
723 let g = param_arrays[i + 3][0] as u8;
724 let b = param_arrays[i + 4][0] as u8;
725 self.attributes.fg_color = Some(TerminalColor::TrueColor { r, g, b });
726 i += 5;
727 continue;
728 }
729 } else if param_arrays[i][0] == 48 && i + 2 < param_arrays.len() {
730 if param_arrays[i + 1].len() == 1
732 && param_arrays[i + 1][0] == 5
733 && param_arrays[i + 2].len() == 1
734 {
735 let color = param_arrays[i + 2][0] as u8;
737 self.attributes.bg_color = Some(TerminalColor::Color256(color));
738 i += 3;
739 continue;
740 } else if param_arrays[i + 1].len() == 1
741 && param_arrays[i + 1][0] == 2
742 && i + 4 < param_arrays.len()
743 && param_arrays[i + 2].len() == 1
744 && param_arrays[i + 3].len() == 1
745 && param_arrays[i + 4].len() == 1
746 {
747 let r = param_arrays[i + 2][0] as u8;
749 let g = param_arrays[i + 3][0] as u8;
750 let b = param_arrays[i + 4][0] as u8;
751 self.attributes.bg_color = Some(TerminalColor::TrueColor { r, g, b });
752 i += 5;
753 continue;
754 }
755 }
756 }
757 i += 1;
758 }
759 }
760 }
761
762 fn handle_osc_params(&mut self, params: &[&[u8]], _bell_terminated: bool) {
764 if params.is_empty() {
765 return;
766 }
767
768 let param_strings: Vec<String> =
770 params.iter().map(|p| String::from_utf8_lossy(p).to_string()).collect();
771
772 if param_strings.is_empty() {
773 return;
774 }
775
776 if param_strings[0] == "8" && param_strings.len() >= 3 {
778 let params =
783 if param_strings.len() > 1 { param_strings[1].clone() } else { String::new() };
784
785 let url =
786 if param_strings.len() > 2 { param_strings[2].clone() } else { String::new() };
787
788 let mut hyperlink_id = None;
790 for param in params.split(':') {
791 let parts: Vec<&str> = param.split('=').collect();
792 if parts.len() == 2 && parts[0] == "id" {
793 hyperlink_id = Some(parts[1].to_string());
794 }
795 }
796
797 if url.is_empty() {
799 self.reset_hyperlink();
801 } else {
802 self.attributes.style.set(CellStyle::HYPERLINK, true);
804 self.attributes.hyperlink_url = Some(url.clone());
805 self.current_hyperlink_url = Some(url);
806
807 if let Some(id) = hyperlink_id {
808 self.current_hyperlink_id = Some(id);
809 }
810 }
811 }
812 }
814
815 fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 {
816 params.iter().nth(index).and_then(|p| p.first().copied()).unwrap_or(default)
817 }
818
819 fn handle_cursor_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
820 let n = usize::from(Self::csi_param(params, 0, 1));
821 let current_row = screen.cursor_row();
822 let current_col = screen.cursor_col();
823
824 match c {
825 'A' => screen.move_cursor(current_row.saturating_sub(n), current_col),
826 'B' => screen.move_cursor(current_row + n, current_col),
827 'C' => screen.move_cursor(current_row, current_col + n),
828 'D' => screen.move_cursor(current_row, current_col.saturating_sub(n)),
829 'H' | 'f' => {
830 let row = usize::from(Self::csi_param(params, 0, 1)).saturating_sub(1);
831 let col = usize::from(Self::csi_param(params, 1, 1)).saturating_sub(1);
832 screen.move_cursor(row, col);
833 }
834 _ => return false,
835 }
836
837 true
838 }
839
840 fn clear_line_to_cursor(screen: &mut Screen) {
841 let row = screen.cursor_row();
842 let col = screen.cursor_col();
843
844 if row < screen.lines.len() {
845 for i in 0..=col.min(screen.lines[row].len().saturating_sub(1)) {
846 screen.lines[row][i] = ScreenCell::default();
847 }
848 }
849 }
850
851 fn handle_erase_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
852 match c {
853 'J' => Self::handle_erase_display(screen, Self::csi_param(params, 0, 0)),
854 'K' => Self::handle_erase_line(screen, Self::csi_param(params, 0, 0)),
855 _ => return false,
856 }
857
858 true
859 }
860
861 fn handle_erase_display(screen: &mut Screen, mode: u16) {
862 match mode {
863 0 => {
864 screen.clear_line_forward();
865 let row = screen.cursor_row();
866 if row + 1 < screen.lines.len() {
867 for i in row + 1..screen.lines.len() {
868 screen.lines[i] = vec![ScreenCell::default(); screen.columns];
869 }
870 }
871 }
872 1 => {
873 Self::clear_line_to_cursor(screen);
874 for i in 0..screen.cursor_row() {
875 if i < screen.lines.len() {
876 screen.lines[i] = vec![ScreenCell::default(); screen.columns];
877 }
878 }
879 }
880 2 | 3 => screen.clear(),
881 _ => debug!("Unhandled erase in display: {}", mode),
882 }
883 }
884
885 fn handle_erase_line(screen: &mut Screen, mode: u16) {
886 match mode {
887 0 => screen.clear_line_forward(),
888 1 => Self::clear_line_to_cursor(screen),
889 2 => screen.clear_line(),
890 _ => debug!("Unhandled erase in line: {}", mode),
891 }
892 }
893
894 fn handle_scroll_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
895 let n = usize::from(Self::csi_param(params, 0, 1));
896
897 match c {
898 'S' => {
899 for _ in 0..n {
900 screen.scroll_up();
901 }
902 }
903 'T' => {
904 let columns = screen.columns;
905 for _ in 0..n {
906 screen.lines.push_front(vec![ScreenCell::default(); columns]);
907 if screen.lines.len() > screen.max_lines {
908 screen.lines.pop_back();
909 }
910 }
911 screen.move_cursor(screen.cursor_row() + n, screen.cursor_col());
912 }
913 _ => return false,
914 }
915
916 true
917 }
918}
919
920impl Perform for TerminalPerformer {
922 fn print(&mut self, c: char) {
923 if let Ok(mut screen) = self.screen.lock() {
924 screen.put_char(c, self.attributes.clone());
925 } else {
926 warn!("Failed to lock screen for print");
927 }
928 }
929
930 fn execute(&mut self, byte: u8) {
931 if let Ok(mut screen) = self.screen.lock() {
932 match byte {
933 b'\r' => screen.carriage_return(),
934 b'\n' => {
935 screen.carriage_return();
936 screen.linefeed();
937 }
938 b'\t' => {
939 let current_col = screen.cursor_col();
941 let new_col = (current_col + 8) & !7;
942 let current_row = screen.cursor_row();
944 screen.move_cursor(current_row, new_col);
945 }
946 b'\x08' => {
947 if screen.cursor_col() > 0 {
949 let current_row = screen.cursor_row();
950 let new_col = screen.cursor_col() - 1;
951 screen.move_cursor(current_row, new_col);
952 }
953 }
954 b'\x0C' => {
955 screen.clear();
957 }
958 b'\x07' => { }
960 _ => {
961 debug!("Unhandled execute: {:?}", byte);
962 }
963 }
964 } else {
965 warn!("Failed to lock screen for execute");
966 }
967 }
968
969 fn hook(&mut self, _params: &vte::Params, _intermediates: &[u8], _ignore: bool, _c: char) {
970 }
972
973 fn put(&mut self, _byte: u8) {
974 }
976
977 fn unhook(&mut self) {
978 }
980
981 fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
982 self.handle_osc_params(params, bell_terminated);
984 }
985
986 fn csi_dispatch(
987 &mut self,
988 params: &vte::Params,
989 _intermediates: &[u8],
990 _ignore: bool,
991 c: char,
992 ) {
993 if c == 'm' {
995 self.handle_sgr_dispatch(params);
996 return;
997 }
998
999 if let Ok(mut screen) = self.screen.lock() {
1000 if Self::handle_cursor_csi(&mut screen, params, c)
1001 || Self::handle_erase_csi(&mut screen, params, c)
1002 || Self::handle_scroll_csi(&mut screen, params, c)
1003 {
1004 return;
1005 }
1006
1007 debug!("Unhandled CSI: {:?} {:?}", params, c);
1008 } else {
1009 warn!("Failed to lock screen for csi_dispatch");
1010 }
1011 }
1012
1013 fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
1014 if intermediates.is_empty() {
1015 match byte {
1016 b'c' => {
1017 if let Ok(mut screen) = self.screen.lock() {
1019 screen.clear();
1020 }
1021 self.reset_attributes();
1022 }
1023 b'7' | b'8' => {
1024 }
1027 _ => debug!("Unhandled ESC dispatch: {:?}", byte),
1028 }
1029 }
1030 }
1031}
1032
1033#[derive(Clone)]
1035pub struct TerminalEmulator {
1036 performer: TerminalPerformer,
1038 screen: Arc<Mutex<Screen>>,
1040}
1041
1042impl std::fmt::Debug for TerminalEmulator {
1044 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1045 f.debug_struct("TerminalEmulator")
1046 .field("performer", &self.performer)
1047 .finish_non_exhaustive()
1048 }
1049}
1050
1051impl TerminalEmulator {
1052 pub fn new(columns: usize) -> Self {
1054 let screen = Arc::new(Mutex::new(Screen::new(columns)));
1055 let performer = TerminalPerformer::new(screen.clone());
1056
1057 Self { performer, screen }
1058 }
1059
1060 pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
1062 let screen = Arc::new(Mutex::new(Screen::new_with_max_lines(columns, max_lines)));
1063 let performer = TerminalPerformer::new(screen.clone());
1064
1065 Self { performer, screen }
1066 }
1067
1068 pub fn process(&mut self, data: &str) {
1070 let mut parser = Parser::new();
1071
1072 let chunk_size = 4096;
1074 let data_bytes = data.as_bytes();
1075
1076 for chunk in data_bytes.chunks(chunk_size) {
1077 parser.advance(&mut self.performer, chunk);
1078 }
1079 }
1080
1081 pub fn process_with_limited_buffer(&mut self, data: &str, max_lines: usize) {
1083 if let Ok(mut screen) = self.screen.lock() {
1084 screen.max_lines = max_lines.min(MAX_SCREEN_LINES);
1086 }
1087
1088 self.process(data);
1089
1090 if let Ok(mut screen) = self.screen.lock() {
1092 if screen.lines.len() > max_lines {
1093 screen.smart_truncate(max_lines);
1094 }
1095 }
1096 }
1097
1098 pub fn get_screen(&self) -> Arc<Mutex<Screen>> {
1100 self.screen.clone()
1101 }
1102
1103 pub fn display(&self) -> Vec<String> {
1105 if let Ok(screen) = self.screen.lock() {
1106 screen.display()
1107 } else {
1108 warn!("Failed to lock screen for display");
1109 vec![]
1110 }
1111 }
1112
1113 pub fn to_plain_text(&self) -> String {
1115 if let Ok(screen) = self.screen.lock() {
1116 screen.to_plain_text()
1117 } else {
1118 warn!("Failed to lock screen for to_plain_text");
1119 String::new()
1120 }
1121 }
1122
1123 pub fn clear(&mut self) {
1125 if let Ok(mut screen) = self.screen.lock() {
1126 screen.clear();
1127 } else {
1128 warn!("Failed to lock screen for clear");
1129 }
1130 }
1131}
1132
1133type CacheEntryMap = HashMap<String, (Vec<String>, Instant)>;
1135
1136#[derive(Debug, Clone)]
1138struct TerminalCache {
1139 entries: Arc<RwLock<CacheEntryMap>>,
1141 max_entries: usize,
1143 ttl: u64,
1145}
1146
1147impl TerminalCache {
1148 fn new(max_entries: usize, ttl: u64) -> Self {
1150 Self { entries: Arc::new(RwLock::new(HashMap::new())), max_entries, ttl }
1151 }
1152
1153 fn get(&self, key: &str) -> Option<Vec<String>> {
1155 if let Ok(entries) = self.entries.read() {
1156 if let Some((value, timestamp)) = entries.get(key) {
1157 if timestamp.elapsed().as_secs() < self.ttl {
1158 return Some(value.clone());
1159 }
1160 }
1161 }
1162 None
1163 }
1164
1165 fn insert(&self, key: String, value: Vec<String>) {
1167 if let Ok(mut entries) = self.entries.write() {
1168 entries.insert(key, (value, Instant::now()));
1170
1171 if entries.len() > self.max_entries {
1173 entries.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < self.ttl);
1175
1176 if entries.len() > self.max_entries {
1178 let mut entries_vec: Vec<_> = entries.iter().collect();
1179 entries_vec.sort_by_key(|(_, (_, timestamp))| *timestamp);
1180
1181 let to_remove = entries_vec.len() - self.max_entries;
1182 let keys_to_remove: Vec<String> =
1183 entries_vec.iter().take(to_remove).map(|(k, _)| (*k).clone()).collect();
1184
1185 for key in keys_to_remove {
1186 entries.remove(&key);
1187 }
1188 }
1189 }
1190 }
1191 }
1192
1193 fn cleanup(&self) {
1195 if let Ok(mut entries) = self.entries.write() {
1196 entries.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < self.ttl);
1197 }
1198 }
1199}
1200
1201lazy_static::lazy_static! {
1203 static ref TERMINAL_CACHE: TerminalCache = TerminalCache::new(100, CACHE_TTL);
1204}
1205
1206#[derive(Debug, Clone)]
1208pub struct TerminalOutputDiff {
1209 previous_output: Vec<String>,
1211 output_hash: String,
1213 max_lines: usize,
1215}
1216
1217impl Default for TerminalOutputDiff {
1218 fn default() -> Self {
1219 Self::new()
1220 }
1221}
1222
1223impl TerminalOutputDiff {
1224 pub fn new() -> Self {
1226 Self { previous_output: Vec::new(), output_hash: String::new(), max_lines: 1000 }
1227 }
1228
1229 pub fn new_with_max_lines(max_lines: usize) -> Self {
1231 Self { previous_output: Vec::new(), output_hash: String::new(), max_lines }
1232 }
1233
1234 pub fn detect_changes(&mut self, new_output: &[String]) -> Vec<String> {
1236 if self.previous_output.is_empty() {
1237 self.previous_output = new_output.to_vec();
1239 self.output_hash = self.calculate_hash(new_output);
1240 return new_output.to_vec();
1241 }
1242
1243 let new_hash = self.calculate_hash(new_output);
1245 if new_hash == self.output_hash {
1246 return Vec::new(); }
1248
1249 let mut changes = Vec::new();
1251
1252 let nold = self.previous_output.len().min(self.max_lines);
1254 let nnew = new_output.len().min(self.max_lines);
1255
1256 let mut matched_position = None;
1258
1259 let is_prefix = nold <= nnew && (0..nold).all(|i| self.previous_output[i] == new_output[i]);
1261
1262 if is_prefix {
1263 matched_position = Some(nold);
1265 } else {
1266 let mut best_match = 0;
1268 let mut best_position = 0;
1269
1270 let window_size = 3.min(nold); if window_size > 0 {
1274 for i in (0..=nnew.saturating_sub(window_size)).rev() {
1275 let mut match_count = 0;
1277 for j in 0..window_size {
1278 if i + j < nnew
1279 && nold.saturating_sub(window_size) + j < nold
1280 && new_output[i + j]
1281 == self.previous_output[nold.saturating_sub(window_size) + j]
1282 {
1283 match_count += 1;
1284 }
1285 }
1286
1287 if match_count > best_match {
1288 best_match = match_count;
1289 best_position = i + window_size;
1290
1291 if best_match == window_size {
1292 break;
1294 }
1295 }
1296 }
1297 }
1298
1299 if best_match >= window_size / 2 {
1300 matched_position = Some(best_position);
1302 }
1303 }
1304
1305 if let Some(pos) = matched_position {
1307 if pos < nnew {
1308 changes = new_output[pos..].to_vec();
1309
1310 if !changes.is_empty()
1312 && !self.previous_output.is_empty()
1313 && changes[0] == self.previous_output[self.previous_output.len() - 1]
1314 {
1315 changes.remove(0);
1316 }
1317 }
1318 } else {
1319 changes = new_output.to_vec();
1321 }
1322
1323 self.previous_output = new_output.to_vec();
1325 self.output_hash = new_hash;
1326
1327 changes
1328 }
1329
1330 fn calculate_hash(&self, lines: &[String]) -> String {
1332 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1335 for line in lines.iter().take(self.max_lines) {
1336 std::hash::Hash::hash(line, &mut hasher);
1337 }
1338 format!("{:x}", std::hash::Hasher::finish(&hasher))
1339 }
1340
1341 pub fn reset(&mut self) {
1343 self.previous_output.clear();
1344 self.output_hash.clear();
1345 }
1346}
1347
1348pub fn render_terminal_output(text: &str) -> Vec<String> {
1350 if let Some(cached) = TERMINAL_CACHE.get(text) {
1352 return cached;
1353 }
1354
1355 let mut terminal = TerminalEmulator::new(DEFAULT_COLUMNS);
1356
1357 if text.len() > MAX_OUTPUT_SIZE {
1359 terminal.process_with_limited_buffer(text, DEFAULT_MAX_SCREEN_LINES);
1361 } else {
1362 terminal.process(text);
1363 }
1364
1365 let result = terminal.display();
1366
1367 if text.len() < MAX_OUTPUT_SIZE {
1369 TERMINAL_CACHE.insert(text.to_string(), result.clone());
1370 }
1371
1372 if rand::random::<u32>() % 100 == 0 {
1374 TERMINAL_CACHE.cleanup();
1375 }
1376
1377 result.into_iter().map(|line| strip_ansi_codes(&line)).collect()
1380}
1381
1382pub fn incremental_text(text: &str, last_pending_output: &str) -> String {
1384 if text.is_empty() {
1386 return String::new();
1387 }
1388
1389 if last_pending_output.is_empty() {
1391 let lines = render_terminal_output(text);
1393 return lines.join("\n").trim().to_string();
1394 }
1395
1396 let is_append = text.starts_with(last_pending_output);
1398
1399 if is_append && text.len() > last_pending_output.len() {
1400 let new_part = &text[last_pending_output.len()..];
1402
1403 let context_len = 200.min(last_pending_output.len());
1405 let full_context = if context_len > 0 {
1406 let start_pos = last_pending_output.len() - context_len;
1407 format!("{}{}", &last_pending_output[start_pos..], new_part)
1408 } else {
1409 new_part.to_string()
1410 };
1411
1412 let previous_lines = render_terminal_output(last_pending_output);
1414 let combined_lines = render_terminal_output(&full_context);
1415
1416 let mut diff_detector = TerminalOutputDiff::new();
1418 diff_detector.previous_output = previous_lines;
1419
1420 let changes = diff_detector.detect_changes(&combined_lines);
1422
1423 if changes.is_empty() {
1424 return String::new();
1425 }
1426
1427 return changes.join("\n");
1428 }
1429
1430 let text_limit = if text.len() > MAX_OUTPUT_SIZE {
1434 let start_offset = text.len() - MAX_OUTPUT_SIZE;
1435
1436 let adjusted_offset =
1438 text[start_offset..].find('\n').map_or(start_offset, |pos| start_offset + pos + 1);
1439
1440 &text[adjusted_offset..]
1441 } else {
1442 text
1443 };
1444
1445 let previous_lines = render_terminal_output(last_pending_output);
1447 let new_lines = render_terminal_output(text_limit);
1448
1449 let mut diff_detector = TerminalOutputDiff::new();
1451 diff_detector.previous_output = previous_lines;
1452
1453 let changes = diff_detector.detect_changes(&new_lines);
1455
1456 if changes.is_empty() {
1457 return String::new();
1458 }
1459
1460 changes.join("\n")
1461}
1462
1463pub fn strip_ansi_codes(input: &str) -> String {
1465 let pattern = r"[\u001b\u009b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]";
1468 match Regex::new(pattern) {
1471 Ok(re) => re.replace_all(input, "").to_string(),
1472 Err(_) => input.replace('\x1b', ""), }
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478 use super::*;
1479
1480 #[test]
1481 fn test_screen_basic_operations() {
1482 let mut screen = Screen::new(80);
1483
1484 let _attributes = ScreenCellAttributes::default();
1486
1487 screen.put_char_basic('H', CellStyle::default(), None, None);
1489 screen.put_char_basic('e', CellStyle::default(), None, None);
1490 screen.put_char_basic('l', CellStyle::default(), None, None);
1491 screen.put_char_basic('l', CellStyle::default(), None, None);
1492 screen.put_char_basic('o', CellStyle::default(), None, None);
1493
1494 let display = screen.display();
1495 assert_eq!(display, vec!["Hello"]);
1496
1497 screen.carriage_return();
1499 screen.linefeed();
1500
1501 screen.put_char_basic('W', CellStyle::default(), None, None);
1502 screen.put_char_basic('o', CellStyle::default(), None, None);
1503 screen.put_char_basic('r', CellStyle::default(), None, None);
1504 screen.put_char_basic('l', CellStyle::default(), None, None);
1505 screen.put_char_basic('d', CellStyle::default(), None, None);
1506
1507 let display = screen.display();
1508 assert_eq!(display, vec!["Hello", "World"]);
1509
1510 screen.clear_line();
1512 let display = screen.display();
1513 assert_eq!(display, vec!["Hello"]);
1514 }
1515
1516 #[test]
1517 fn test_terminal_emulator_basic() {
1518 let mut terminal = TerminalEmulator::new(80);
1519
1520 terminal.process("Hello\r\nWorld");
1522 let display = terminal.display();
1523 assert_eq!(display, vec!["Hello", "World"]);
1524
1525 terminal.clear();
1527 terminal.process("Normal \x1b[1mBold\x1b[0m Normal");
1528 let display = terminal.display();
1529 assert_eq!(display, vec!["Normal Bold Normal"]);
1530
1531 terminal.clear();
1533 terminal.process("Hello\x1b[5D_\x1b[1C_\x1b[1C_");
1534 let display = terminal.display();
1535 assert_eq!(display, vec!["_e_l_"]);
1536 }
1537
1538 #[test]
1539 fn test_incremental_output() {
1540 let old = vec!["Line 1".to_string(), "Line 2".to_string()];
1541 let new = vec!["Line 1".to_string(), "Line 2".to_string(), "Line 3".to_string()];
1542
1543 let mut diff_detector = TerminalOutputDiff::new();
1544 diff_detector.previous_output = old;
1545
1546 let incremental = diff_detector.detect_changes(&new);
1547 assert_eq!(incremental, vec!["Line 3"]);
1548
1549 let old = vec!["Line A".to_string(), "Line B".to_string()];
1551 let new = vec!["Line X".to_string(), "Line Y".to_string()];
1552
1553 let mut diff_detector = TerminalOutputDiff::new();
1554 diff_detector.previous_output = old;
1555
1556 let incremental = diff_detector.detect_changes(&new);
1557 assert_eq!(incremental, vec!["Line X", "Line Y"]);
1558 }
1559
1560 #[test]
1561 fn test_render_terminal_output() {
1562 let text = "Hello\r\nWorld\r\n\x1b[31mRed\x1b[0m Text";
1563 let lines = render_terminal_output(text);
1564 assert_eq!(lines, vec!["Hello", "World", "Red Text"]);
1565 }
1566
1567 #[test]
1568 fn test_smart_truncate() {
1569 let mut screen = Screen::new_with_max_lines(80, 20);
1570
1571 for i in 0..30 {
1573 let line = format!("Line {i}");
1574 for c in line.chars() {
1575 screen.put_char(c, ScreenCellAttributes::default());
1576 }
1577 screen.carriage_return();
1578 screen.linefeed();
1579 }
1580
1581 assert_eq!(screen.lines.len(), 20);
1583
1584 screen.smart_truncate(10);
1586
1587 assert_eq!(screen.lines.len(), 10);
1589
1590 let has_truncation_marker = screen.lines.iter().any(|line| {
1592 let line_text: String = line.iter().map(|cell| cell.character).collect();
1593 line_text.contains("TRUNCATED")
1594 });
1595
1596 assert!(has_truncation_marker);
1597 }
1598
1599 #[test]
1600 fn test_terminal_cache() {
1601 let cache = TerminalCache::new(10, 60);
1602
1603 cache.insert("test".to_string(), vec!["line1".to_string(), "line2".to_string()]);
1605
1606 let retrieved = cache.get("test");
1608 assert_eq!(retrieved, Some(vec!["line1".to_string(), "line2".to_string()]));
1609
1610 let not_found = cache.get("unknown");
1612 assert_eq!(not_found, None);
1613 }
1614
1615 #[test]
1616 fn test_incremental_text_append() {
1617 let old_text = "Line 1\nLine 2\n";
1618 let new_text = "Line 1\nLine 2\nLine 3\n";
1619
1620 let incremental = incremental_text(new_text, old_text);
1621 assert_eq!(incremental, "Line 3");
1622 }
1623
1624 #[test]
1625 fn test_terminal_color_handling() {
1626 let mut terminal = TerminalEmulator::new(80);
1627
1628 terminal.process("\x1b[31mRed\x1b[32mGreen\x1b[0mNormal");
1630 let display = terminal.display();
1631 assert_eq!(display, vec!["RedGreenNormal"]);
1632
1633 terminal.clear();
1635 terminal.process("\x1b[38;5;208mOrange\x1b[0mNormal");
1636 let display = terminal.display();
1637 assert_eq!(display, vec!["OrangeNormal"]);
1638 }
1639}