Skip to main content

winx_code_agent/state/
terminal.rs

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// Import our enhanced ANSI code module
9#[allow(unused_imports)]
10use crate::state::ansi_codes;
11
12/// Maximum number of lines to keep in the screen buffer
13pub const MAX_SCREEN_LINES: usize = 10000;
14/// Default maximum number of lines to keep in the screen buffer
15pub const DEFAULT_MAX_SCREEN_LINES: usize = 500;
16/// Maximum number of columns for the screen
17const DEFAULT_COLUMNS: usize = 160;
18/// Maximum output size in bytes to prevent excessive memory usage
19pub const MAX_OUTPUT_SIZE: usize = 500_000;
20/// Maximum cache entry lifetime in seconds
21const CACHE_TTL: u64 = 300; // 5 minutes
22
23/// Compact bitset for terminal text styles.
24#[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/// Container for all possible character attributes
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
66pub struct ScreenCellAttributes {
67    /// Text style flags such as bold, underline, reverse, and blink.
68    pub style: CellStyle,
69    /// Foreground color
70    pub fg_color: Option<TerminalColor>,
71    /// Background color
72    pub bg_color: Option<TerminalColor>,
73    /// URL for hyperlink, if applicable
74    pub hyperlink_url: Option<String>,
75    /// Font selection (0-9, where 0 is the primary font)
76    pub font: u8,
77}
78
79/// Represents a character with attributes in the terminal
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ScreenCell {
82    /// The character to display
83    pub character: char,
84    /// Text style flags such as bold, underline, reverse, and blink.
85    pub style: CellStyle,
86    /// Foreground color (0-255 for 8-bit colors, RGB for 24-bit colors)
87    pub fg_color: Option<TerminalColor>,
88    /// Background color (0-255 for 8-bit colors, RGB for 24-bit colors)
89    pub bg_color: Option<TerminalColor>,
90    /// URL for hyperlink, if applicable
91    pub hyperlink_url: Option<String>,
92    /// Font selection (0-9, where 0 is the primary font)
93    pub font: u8,
94}
95
96/// Represents a terminal color
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum TerminalColor {
99    /// Basic 8 colors (0-7)
100    Basic(u8),
101    /// Extended 8-bit color (0-255)
102    Color256(u8),
103    /// 24-bit RGB color
104    TrueColor { r: u8, g: u8, b: u8 },
105    /// Named color like "red", "blue", etc.
106    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, // Primary font
131        }
132    }
133}
134
135/// Represents the current state of a terminal screen
136#[derive(Debug, Clone)]
137pub struct Screen {
138    /// Lines of characters with attributes
139    pub lines: VecDeque<Vec<ScreenCell>>,
140    /// Current cursor position (row, column)
141    pub cursor_position: (usize, usize),
142    /// Number of columns in the screen
143    pub columns: usize,
144    /// Whether the cursor should be visible
145    pub cursor_visible: bool,
146    /// Maximum number of lines to keep
147    pub max_lines: usize,
148    /// Last time the screen was modified
149    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    /// Creates a new screen with specified dimensions
170    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    /// Creates a new screen with specified dimensions and maximum lines
185    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    /// Get the current cursor row
200    pub fn cursor_row(&self) -> usize {
201        self.cursor_position.0
202    }
203
204    /// Get the current cursor column
205    pub fn cursor_col(&self) -> usize {
206        self.cursor_position.1
207    }
208
209    /// Ensure that a line exists at the specified index
210    fn ensure_line(&mut self, line_idx: usize) {
211        // Add new lines if needed
212        while self.lines.len() <= line_idx {
213            self.lines.push_back(vec![ScreenCell::default(); self.columns]);
214        }
215
216        // Limit the number of lines to prevent memory growth
217        while self.lines.len() > self.max_lines {
218            self.lines.pop_front();
219
220            // Adjust cursor position to account for the removed line
221            if self.cursor_position.0 > 0 {
222                self.cursor_position.0 -= 1;
223            }
224        }
225
226        self.last_modified = Instant::now();
227    }
228
229    /// Ensure that the cursor position is valid
230    fn ensure_cursor_position(&mut self) {
231        self.ensure_line(self.cursor_position.0);
232
233        // Ensure the cursor column is within bounds
234        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    /// Put a character at the current cursor position and advance the cursor
242    pub fn put_char(&mut self, c: char, attributes: ScreenCellAttributes) {
243        self.ensure_cursor_position();
244
245        // Get the current cursor position
246        let row = self.cursor_position.0;
247        let col = self.cursor_position.1;
248
249        // Put the character at the cursor position
250        if col < self.lines[row].len() {
251            self.lines[row][col] = ScreenCell::new(c, attributes);
252        } else {
253            // Add cells if needed
254            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        // Advance the cursor
261        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    /// Put a character at the current cursor position with basic attributes
272    #[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    /// Move the cursor to a specific position
286    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    /// Add a new line at the cursor position
293    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    /// Return the cursor to the first column
300    pub fn carriage_return(&mut self) {
301        self.cursor_position.1 = 0;
302        self.last_modified = Instant::now();
303    }
304
305    /// Clear the screen
306    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    /// Clear from the cursor to the end of the line
314    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    /// Clear the current line
327    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    /// Scroll the screen up by one line
336    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    /// Smart truncate the screen buffer to keep it within reasonable limits
345    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        // Calculate how many lines to remove
353        let to_remove = current_size - max_size;
354
355        // Keep a reasonable amount at the beginning
356        let beginning_lines = max_size / 10; // 10% of max size
357
358        if to_remove <= beginning_lines {
359            // Simple case: just remove from the beginning
360            for _ in 0..to_remove {
361                self.lines.pop_front();
362            }
363        } else {
364            // Complex case: keep beginning and end, with a marker in the middle
365            let end_lines = max_size - beginning_lines - 1; // -1 for truncation marker
366
367            // Save important parts
368            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            // Clear and rebuild with beginning + marker + end
375            self.lines.clear();
376
377            // Add beginning
378            for line in beginning {
379                self.lines.push_back(line);
380            }
381
382            // Add truncation marker
383            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            // Add end
399            for line in end {
400                self.lines.push_back(line);
401            }
402        }
403
404        // Adjust cursor position if necessary
405        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    /// Get the screen as plain text
413    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    /// Get the screen as a vector of strings, with each string representing a line
426    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            // Trim trailing spaces
433            let trimmed = line_text.trim_end();
434            result.push(trimmed.to_string());
435        }
436
437        // Remove empty lines from the end
438        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    /// Returns the last time the screen was modified
450    pub fn last_modified(&self) -> Instant {
451        self.last_modified
452    }
453
454    /// Time since last modification in seconds
455    pub fn time_since_last_modified(&self) -> f64 {
456        self.last_modified.elapsed().as_secs_f64()
457    }
458}
459
460/// Terminal state performer that handles VTE events
461#[derive(Clone)]
462pub struct TerminalPerformer {
463    /// The screen state
464    screen: Arc<Mutex<Screen>>,
465    /// Current text attributes
466    attributes: ScreenCellAttributes,
467    /// SGR parameters cache for optimization
468    sgr_state: HashMap<u16, bool>,
469    /// Active hyperlink ID, if any
470    current_hyperlink_id: Option<String>,
471    /// Active hyperlink URL, if any
472    current_hyperlink_url: Option<String>,
473    /// Current OSC parameters being parsed
474    osc_params: Vec<String>,
475}
476
477// Custom debug implementation to avoid using the one from VTE
478impl 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    /// Creates a new terminal performer
490    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    /// Get a reference to the screen
502    pub fn screen(&self) -> &Arc<Mutex<Screen>> {
503        &self.screen
504    }
505
506    /// Reset all text attributes
507    fn reset_attributes(&mut self) {
508        self.attributes = ScreenCellAttributes::default();
509        self.sgr_state.clear();
510    }
511
512    /// Reset hyperlink state
513    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    /// Parse and handle SGR (Select Graphic Rendition) parameters
531    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
689// Additional methods for TerminalPerformer outside of the Perform trait
690impl TerminalPerformer {
691    /// Handle SGR (Select Graphic Rendition) parameters and extended color sequences
692    fn handle_sgr_dispatch(&mut self, params: &vte::Params) {
693        // Process the basic SGR parameters
694        self.handle_sgr_params(params);
695
696        // Handle extended color params (38, 48) manually since they require sequences
697        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                        // Extended foreground color
705                        if param_arrays[i + 1].len() == 1
706                            && param_arrays[i + 1][0] == 5
707                            && param_arrays[i + 2].len() == 1
708                        {
709                            // 8-bit color (256 colors)
710                            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                            // 24-bit RGB color
722                            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                        // Extended background color
731                        if param_arrays[i + 1].len() == 1
732                            && param_arrays[i + 1][0] == 5
733                            && param_arrays[i + 2].len() == 1
734                        {
735                            // 8-bit color (256 colors)
736                            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                            // 24-bit RGB color
748                            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    /// Handle OSC (Operating System Command) sequences
763    fn handle_osc_params(&mut self, params: &[&[u8]], _bell_terminated: bool) {
764        if params.is_empty() {
765            return;
766        }
767
768        // Convert the params to strings for easier handling
769        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        // Handle known OSC sequences
777        if param_strings[0] == "8" && param_strings.len() >= 3 {
778            // OSC 8: Hyperlink
779            // Format: OSC 8 ; params ; URI ST
780
781            // Get hyperlink parameters and URL
782            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            // Parse parameters (id=value format)
789            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            // Handle hyperlinks
798            if url.is_empty() {
799                // Empty URL means end of hyperlink
800                self.reset_hyperlink();
801            } else {
802                // Start of hyperlink
803                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        // Add support for other OSC sequences here (window title, color definitions, etc.)
813    }
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
920// Implement the VTE Perform trait
921impl 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                    // Handle tab - advance to next 8-char boundary
940                    let current_col = screen.cursor_col();
941                    let new_col = (current_col + 8) & !7;
942                    // Get the current row first to avoid multiple borrows
943                    let current_row = screen.cursor_row();
944                    screen.move_cursor(current_row, new_col);
945                }
946                b'\x08' => {
947                    // Backspace
948                    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                    // Form feed - clear screen
956                    screen.clear();
957                }
958                b'\x07' => { // Bell - ignore
959                }
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        // Not implemented
971    }
972
973    fn put(&mut self, _byte: u8) {
974        // Not implemented
975    }
976
977    fn unhook(&mut self) {
978        // Not implemented
979    }
980
981    fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
982        // Implement OSC parameter handling
983        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        // Special case for SGR ('m') to avoid borrowing conflict
994        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                    // RIS - Reset to Initial State
1018                    if let Ok(mut screen) = self.screen.lock() {
1019                        screen.clear();
1020                    }
1021                    self.reset_attributes();
1022                }
1023                b'7' | b'8' => {
1024                    // DECSC/DECRC - Save/restore cursor
1025                    // Not implemented yet
1026                }
1027                _ => debug!("Unhandled ESC dispatch: {:?}", byte),
1028            }
1029        }
1030    }
1031}
1032
1033/// Terminal emulator that processes input and maintains screen state
1034#[derive(Clone)]
1035pub struct TerminalEmulator {
1036    /// The performer that handles terminal events
1037    performer: TerminalPerformer,
1038    /// The shared screen state
1039    screen: Arc<Mutex<Screen>>,
1040}
1041
1042// Custom debug implementation to avoid issues with Parser
1043impl 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    /// Creates a new terminal emulator
1053    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    /// Creates a new terminal emulator with specified maximum lines
1061    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    /// Process input and update screen state
1069    pub fn process(&mut self, data: &str) {
1070        let mut parser = Parser::new();
1071
1072        // Process data in chunks to avoid excessive locking
1073        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    /// Process input with limited buffer (for large outputs)
1082    pub fn process_with_limited_buffer(&mut self, data: &str, max_lines: usize) {
1083        if let Ok(mut screen) = self.screen.lock() {
1084            // Update max_lines setting
1085            screen.max_lines = max_lines.min(MAX_SCREEN_LINES);
1086        }
1087
1088        self.process(data);
1089
1090        // After processing, check if we need to smart truncate
1091        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    /// Get the current screen state
1099    pub fn get_screen(&self) -> Arc<Mutex<Screen>> {
1100        self.screen.clone()
1101    }
1102
1103    /// Get the screen contents as a vector of strings
1104    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    /// Get the screen contents as plain text
1114    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    /// Clear the screen
1124    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
1133/// Type definition for cache entries to simplify complex types
1134type CacheEntryMap = HashMap<String, (Vec<String>, Instant)>;
1135
1136/// Caching system for terminal output rendering
1137#[derive(Debug, Clone)]
1138struct TerminalCache {
1139    /// Cache entries mapping text content to rendered output
1140    entries: Arc<RwLock<CacheEntryMap>>,
1141    /// Maximum number of entries in the cache
1142    max_entries: usize,
1143    /// Time-to-live for cache entries in seconds
1144    ttl: u64,
1145}
1146
1147impl TerminalCache {
1148    /// Create a new terminal cache
1149    fn new(max_entries: usize, ttl: u64) -> Self {
1150        Self { entries: Arc::new(RwLock::new(HashMap::new())), max_entries, ttl }
1151    }
1152
1153    /// Get a cached value if available and not expired
1154    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    /// Insert a value into the cache
1166    fn insert(&self, key: String, value: Vec<String>) {
1167        if let Ok(mut entries) = self.entries.write() {
1168            // Insert the new entry
1169            entries.insert(key, (value, Instant::now()));
1170
1171            // Clean up old entries if cache is too large
1172            if entries.len() > self.max_entries {
1173                // Remove expired entries first
1174                entries.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < self.ttl);
1175
1176                // If still too many entries, remove oldest
1177                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    /// Clear expired entries from the cache
1194    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
1201// Initialize the global terminal cache
1202lazy_static::lazy_static! {
1203    static ref TERMINAL_CACHE: TerminalCache = TerminalCache::new(100, CACHE_TTL);
1204}
1205
1206/// Terminal output difference detector
1207#[derive(Debug, Clone)]
1208pub struct TerminalOutputDiff {
1209    /// Previous output lines
1210    previous_output: Vec<String>,
1211    /// Hash of previous output
1212    output_hash: String,
1213    /// Maximum number of lines to compare
1214    max_lines: usize,
1215}
1216
1217impl Default for TerminalOutputDiff {
1218    fn default() -> Self {
1219        Self::new()
1220    }
1221}
1222
1223impl TerminalOutputDiff {
1224    /// Create a new terminal output diff detector
1225    pub fn new() -> Self {
1226        Self { previous_output: Vec::new(), output_hash: String::new(), max_lines: 1000 }
1227    }
1228
1229    /// Create a new terminal output diff detector with specified maximum lines
1230    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    /// Detect changes between previous and new output
1235    pub fn detect_changes(&mut self, new_output: &[String]) -> Vec<String> {
1236        if self.previous_output.is_empty() {
1237            // First run, just return all lines
1238            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        // Check if output is identical (fast path)
1244        let new_hash = self.calculate_hash(new_output);
1245        if new_hash == self.output_hash {
1246            return Vec::new(); // No changes
1247        }
1248
1249        // Find differences
1250        let mut changes = Vec::new();
1251
1252        // Find where new content starts
1253        let nold = self.previous_output.len().min(self.max_lines);
1254        let nnew = new_output.len().min(self.max_lines);
1255
1256        // Try to find where old output ends and new output begins using a more efficient algorithm
1257        let mut matched_position = None;
1258
1259        // Check if new output contains all of old output as a prefix
1260        let is_prefix = nold <= nnew && (0..nold).all(|i| self.previous_output[i] == new_output[i]);
1261
1262        if is_prefix {
1263            // Simple case: new output is old output plus additions
1264            matched_position = Some(nold);
1265        } else {
1266            // More complex case: try to find the last matching block
1267            let mut best_match = 0;
1268            let mut best_position = 0;
1269
1270            // Use sliding window approach to find largest match
1271            let window_size = 3.min(nold); // Use 3 lines as context for matching
1272
1273            if window_size > 0 {
1274                for i in (0..=nnew.saturating_sub(window_size)).rev() {
1275                    // Try matching last window_size lines of old output with window at position i in new output
1276                    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                            // Perfect match, no need to continue
1293                            break;
1294                        }
1295                    }
1296                }
1297            }
1298
1299            if best_match >= window_size / 2 {
1300                // Found a reasonable match
1301                matched_position = Some(best_position);
1302            }
1303        }
1304
1305        // Extract changes based on matched position
1306        if let Some(pos) = matched_position {
1307            if pos < nnew {
1308                changes = new_output[pos..].to_vec();
1309
1310                // Check if first line of changes matches last line of previous output
1311                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            // Fallback: couldn't find a good match, show all new lines
1320            changes = new_output.to_vec();
1321        }
1322
1323        // Update state for next comparison
1324        self.previous_output = new_output.to_vec();
1325        self.output_hash = new_hash;
1326
1327        changes
1328    }
1329
1330    /// Calculate a hash of the output lines for quick comparison
1331    fn calculate_hash(&self, lines: &[String]) -> String {
1332        // Simple hash function based on content
1333        // In a production setting, use a proper hash function
1334        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    /// Reset the diff detector
1342    pub fn reset(&mut self) {
1343        self.previous_output.clear();
1344        self.output_hash.clear();
1345    }
1346}
1347
1348/// Render terminal output with line wrapping
1349pub fn render_terminal_output(text: &str) -> Vec<String> {
1350    // Check cache first
1351    if let Some(cached) = TERMINAL_CACHE.get(text) {
1352        return cached;
1353    }
1354
1355    let mut terminal = TerminalEmulator::new(DEFAULT_COLUMNS);
1356
1357    // Check if we need to limit processing for large outputs
1358    if text.len() > MAX_OUTPUT_SIZE {
1359        // For large outputs, use limited buffer mode
1360        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    // Cache the result for future use (only if reasonably sized)
1368    if text.len() < MAX_OUTPUT_SIZE {
1369        TERMINAL_CACHE.insert(text.to_string(), result.clone());
1370    }
1371
1372    // Periodically clean up expired cache entries
1373    if rand::random::<u32>() % 100 == 0 {
1374        TERMINAL_CACHE.cleanup();
1375    }
1376
1377    // IMPORTANT: Strip any remaining ANSI codes from the result lines to prevent JSON-RPC errors.
1378    // This is the core fix for "invalid character '\x1b'" errors.
1379    result.into_iter().map(|line| strip_ansi_codes(&line)).collect()
1380}
1381
1382/// Get incremental text output by comparing old and new terminal states
1383pub fn incremental_text(text: &str, last_pending_output: &str) -> String {
1384    // Optimization: Quick check for empty input
1385    if text.is_empty() {
1386        return String::new();
1387    }
1388
1389    // Optimization: If last output is empty, just process everything
1390    if last_pending_output.is_empty() {
1391        // First call, return all processed lines with leading/trailing whitespace trimmed
1392        let lines = render_terminal_output(text);
1393        return lines.join("\n").trim().to_string();
1394    }
1395
1396    // Optimization: Handle case where new text is just appended to old text
1397    let is_append = text.starts_with(last_pending_output);
1398
1399    if is_append && text.len() > last_pending_output.len() {
1400        // Incremental case - only process the new part
1401        let new_part = &text[last_pending_output.len()..];
1402
1403        // Ensure we have enough context by including a bit more than just the new part
1404        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        // Process the combined output for context
1413        let previous_lines = render_terminal_output(last_pending_output);
1414        let combined_lines = render_terminal_output(&full_context);
1415
1416        // Create a diff detector for efficient comparison
1417        let mut diff_detector = TerminalOutputDiff::new();
1418        diff_detector.previous_output = previous_lines;
1419
1420        // Get just the changes
1421        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    // Fallback for non-append cases:
1431
1432    // Limit text size to prevent excessive memory usage
1433    let text_limit = if text.len() > MAX_OUTPUT_SIZE {
1434        let start_offset = text.len() - MAX_OUTPUT_SIZE;
1435
1436        // Find the start of a line to avoid cutting in the middle
1437        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    // Process both old and new output
1446    let previous_lines = render_terminal_output(last_pending_output);
1447    let new_lines = render_terminal_output(text_limit);
1448
1449    // Create a diff detector for efficient comparison
1450    let mut diff_detector = TerminalOutputDiff::new();
1451    diff_detector.previous_output = previous_lines;
1452
1453    // Get the incremental changes
1454    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
1463/// Strip ANSI escape codes from a string using a robust regex
1464pub fn strip_ansi_codes(input: &str) -> String {
1465    // Regex to match ANSI escape codes
1466    // Matches most common CSI and other sequences
1467    let pattern = r"[\u001b\u009b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]";
1468    // Regex compilation is expensive for single use, but this effectively sanitizes output
1469    // preventing JSON RPC crashes.
1470    match Regex::new(pattern) {
1471        Ok(re) => re.replace_all(input, "").to_string(),
1472        Err(_) => input.replace('\x1b', ""), // Fallback
1473    }
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        // Create default attributes
1485        let _attributes = ScreenCellAttributes::default();
1486
1487        // Test putting characters
1488        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        // Test cursor movement
1498        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        // Test clearing line
1511        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        // Test simple text processing
1521        terminal.process("Hello\r\nWorld");
1522        let display = terminal.display();
1523        assert_eq!(display, vec!["Hello", "World"]);
1524
1525        // Test escape sequences
1526        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        // Test cursor movement
1532        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        // Test with completely different content
1550        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        // Add 30 lines of content
1572        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        // Should have truncated to 20 lines
1582        assert_eq!(screen.lines.len(), 20);
1583
1584        // Now test smart truncate
1585        screen.smart_truncate(10);
1586
1587        // Should now have 10 lines
1588        assert_eq!(screen.lines.len(), 10);
1589
1590        // One of the lines should be the truncation marker
1591        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        // Insert a value
1604        cache.insert("test".to_string(), vec!["line1".to_string(), "line2".to_string()]);
1605
1606        // Should be able to retrieve it
1607        let retrieved = cache.get("test");
1608        assert_eq!(retrieved, Some(vec!["line1".to_string(), "line2".to_string()]));
1609
1610        // Unknown key should return None
1611        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        // Test basic colors
1629        terminal.process("\x1b[31mRed\x1b[32mGreen\x1b[0mNormal");
1630        let display = terminal.display();
1631        assert_eq!(display, vec!["RedGreenNormal"]);
1632
1633        // Test 256 colors
1634        terminal.clear();
1635        terminal.process("\x1b[38;5;208mOrange\x1b[0mNormal");
1636        let display = terminal.display();
1637        assert_eq!(display, vec!["OrangeNormal"]);
1638    }
1639}