use regex::Regex;
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
use tracing::{debug, warn};
use vte::{Parser, Perform};
#[allow(unused_imports)]
use crate::state::ansi_codes;
pub const MAX_SCREEN_LINES: usize = 10000;
pub const DEFAULT_MAX_SCREEN_LINES: usize = 500;
const DEFAULT_COLUMNS: usize = 160;
pub const MAX_OUTPUT_SIZE: usize = 500_000;
const CACHE_TTL: u64 = 300;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellStyle(u32);
impl CellStyle {
pub const BOLD: Self = Self(1 << 0);
pub const UNDERLINE: Self = Self(1 << 1);
pub const BLINK: Self = Self(1 << 2);
pub const REVERSE: Self = Self(1 << 3);
pub const ITALIC: Self = Self(1 << 4);
pub const STRIKETHROUGH: Self = Self(1 << 5);
pub const DIM: Self = Self(1 << 6);
pub const DOUBLE_UNDERLINE: Self = Self(1 << 7);
pub const FRAMED: Self = Self(1 << 8);
pub const ENCIRCLED: Self = Self(1 << 9);
pub const OVERLINED: Self = Self(1 << 10);
pub const FRAKTUR: Self = Self(1 << 11);
pub const CONCEAL: Self = Self(1 << 12);
pub const SUPERSCRIPT: Self = Self(1 << 13);
pub const SUBSCRIPT: Self = Self(1 << 14);
pub const HYPERLINK: Self = Self(1 << 15);
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn contains(self, flag: Self) -> bool {
self.0 & flag.0 != 0
}
pub fn set(&mut self, flag: Self, enabled: bool) {
if enabled {
self.0 |= flag.0;
} else {
self.0 &= !flag.0;
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScreenCellAttributes {
pub style: CellStyle,
pub fg_color: Option<TerminalColor>,
pub bg_color: Option<TerminalColor>,
pub hyperlink_url: Option<String>,
pub font: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScreenCell {
pub character: char,
pub style: CellStyle,
pub fg_color: Option<TerminalColor>,
pub bg_color: Option<TerminalColor>,
pub hyperlink_url: Option<String>,
pub font: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalColor {
Basic(u8),
Color256(u8),
TrueColor { r: u8, g: u8, b: u8 },
Named(String),
}
impl ScreenCell {
fn new(character: char, attributes: ScreenCellAttributes) -> Self {
Self {
character,
style: attributes.style,
fg_color: attributes.fg_color,
bg_color: attributes.bg_color,
hyperlink_url: attributes.hyperlink_url,
font: attributes.font,
}
}
}
impl Default for ScreenCell {
fn default() -> Self {
Self {
character: ' ',
style: CellStyle::default(),
fg_color: None,
bg_color: None,
hyperlink_url: None,
font: 0, }
}
}
#[derive(Debug, Clone)]
pub struct Screen {
pub lines: VecDeque<Vec<ScreenCell>>,
pub cursor_position: (usize, usize),
pub columns: usize,
pub cursor_visible: bool,
pub max_lines: usize,
last_modified: Instant,
}
impl Default for Screen {
fn default() -> Self {
let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
lines.push_back(vec![ScreenCell::default(); DEFAULT_COLUMNS]);
Self {
lines,
cursor_position: (0, 0),
columns: DEFAULT_COLUMNS,
cursor_visible: true,
max_lines: DEFAULT_MAX_SCREEN_LINES,
last_modified: Instant::now(),
}
}
}
impl Screen {
pub fn new(columns: usize) -> Self {
let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
lines.push_back(vec![ScreenCell::default(); columns]);
Self {
lines,
cursor_position: (0, 0),
columns,
cursor_visible: true,
max_lines: DEFAULT_MAX_SCREEN_LINES,
last_modified: Instant::now(),
}
}
pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
let mut lines = VecDeque::with_capacity(max_lines.min(MAX_SCREEN_LINES));
lines.push_back(vec![ScreenCell::default(); columns]);
Self {
lines,
cursor_position: (0, 0),
columns,
cursor_visible: true,
max_lines: max_lines.min(MAX_SCREEN_LINES),
last_modified: Instant::now(),
}
}
pub fn cursor_row(&self) -> usize {
self.cursor_position.0
}
pub fn cursor_col(&self) -> usize {
self.cursor_position.1
}
fn ensure_line(&mut self, line_idx: usize) {
while self.lines.len() <= line_idx {
self.lines.push_back(vec![ScreenCell::default(); self.columns]);
}
while self.lines.len() > self.max_lines {
self.lines.pop_front();
if self.cursor_position.0 > 0 {
self.cursor_position.0 -= 1;
}
}
self.last_modified = Instant::now();
}
fn ensure_cursor_position(&mut self) {
self.ensure_line(self.cursor_position.0);
if self.cursor_position.1 >= self.columns {
self.cursor_position.1 = self.columns - 1;
}
self.last_modified = Instant::now();
}
pub fn put_char(&mut self, c: char, attributes: ScreenCellAttributes) {
self.ensure_cursor_position();
let row = self.cursor_position.0;
let col = self.cursor_position.1;
if col < self.lines[row].len() {
self.lines[row][col] = ScreenCell::new(c, attributes);
} else {
while self.lines[row].len() <= col {
self.lines[row].push(ScreenCell::default());
}
self.lines[row][col] = ScreenCell::new(c, attributes);
}
self.cursor_position.1 += 1;
if self.cursor_position.1 >= self.columns {
self.cursor_position.1 = 0;
self.cursor_position.0 += 1;
self.ensure_cursor_position();
}
self.last_modified = Instant::now();
}
#[allow(clippy::too_many_arguments)]
pub fn put_char_basic(
&mut self,
c: char,
style: CellStyle,
fg_color: Option<TerminalColor>,
bg_color: Option<TerminalColor>,
) {
let attributes = ScreenCellAttributes { style, fg_color, bg_color, ..Default::default() };
self.put_char(c, attributes);
}
pub fn move_cursor(&mut self, row: usize, col: usize) {
self.cursor_position = (row, col);
self.ensure_cursor_position();
self.last_modified = Instant::now();
}
pub fn linefeed(&mut self) {
self.cursor_position.0 += 1;
self.ensure_cursor_position();
self.last_modified = Instant::now();
}
pub fn carriage_return(&mut self) {
self.cursor_position.1 = 0;
self.last_modified = Instant::now();
}
pub fn clear(&mut self) {
self.lines.clear();
self.lines.push_back(vec![ScreenCell::default(); self.columns]);
self.cursor_position = (0, 0);
self.last_modified = Instant::now();
}
pub fn clear_line_forward(&mut self) {
let row = self.cursor_position.0;
let col = self.cursor_position.1;
if row < self.lines.len() {
for i in col..self.lines[row].len() {
self.lines[row][i] = ScreenCell::default();
}
}
self.last_modified = Instant::now();
}
pub fn clear_line(&mut self) {
let row = self.cursor_position.0;
if row < self.lines.len() {
self.lines[row] = vec![ScreenCell::default(); self.columns];
}
self.last_modified = Instant::now();
}
pub fn scroll_up(&mut self) {
if !self.lines.is_empty() {
self.lines.pop_front();
self.ensure_line(self.cursor_position.0);
}
self.last_modified = Instant::now();
}
pub fn smart_truncate(&mut self, max_size: usize) {
let current_size = self.lines.len();
if current_size <= max_size {
return;
}
let to_remove = current_size - max_size;
let beginning_lines = max_size / 10;
if to_remove <= beginning_lines {
for _ in 0..to_remove {
self.lines.pop_front();
}
} else {
let end_lines = max_size - beginning_lines - 1;
let beginning: VecDeque<Vec<ScreenCell>> =
self.lines.drain(0..beginning_lines.min(self.lines.len())).collect();
let end_start_index = self.lines.len().saturating_sub(end_lines);
let end: VecDeque<Vec<ScreenCell>> = self.lines.drain(end_start_index..).collect();
self.lines.clear();
for line in beginning {
self.lines.push_back(line);
}
let mut marker_line = vec![ScreenCell::default(); self.columns];
let marker_text = " [... TRUNCATED OUTPUT ...] ";
for (i, c) in marker_text.chars().enumerate() {
if i < self.columns {
marker_line[i] = ScreenCell {
character: c,
style: CellStyle::BOLD.union(CellStyle::REVERSE),
..ScreenCell::default()
};
}
}
self.lines.push_back(marker_line);
for line in end {
self.lines.push_back(line);
}
}
if self.cursor_position.0 >= self.lines.len() {
self.cursor_position.0 = self.lines.len().saturating_sub(1);
}
self.last_modified = Instant::now();
}
pub fn to_plain_text(&self) -> String {
let mut result = String::with_capacity(self.lines.len() * self.columns);
for line in &self.lines {
let line_text: String = line.iter().map(|cell| cell.character).collect();
result.push_str(&line_text);
result.push('\n');
}
result
}
pub fn display(&self) -> Vec<String> {
let mut result = Vec::with_capacity(self.lines.len());
for line in &self.lines {
let line_text: String = line.iter().map(|cell| cell.character).collect();
let trimmed = line_text.trim_end();
result.push(trimmed.to_string());
}
while let Some(last) = result.last() {
if last.is_empty() {
result.pop();
} else {
break;
}
}
result
}
pub fn last_modified(&self) -> Instant {
self.last_modified
}
pub fn time_since_last_modified(&self) -> f64 {
self.last_modified.elapsed().as_secs_f64()
}
}
#[derive(Clone)]
pub struct TerminalPerformer {
screen: Arc<Mutex<Screen>>,
attributes: ScreenCellAttributes,
sgr_state: HashMap<u16, bool>,
current_hyperlink_id: Option<String>,
current_hyperlink_url: Option<String>,
osc_params: Vec<String>,
}
impl std::fmt::Debug for TerminalPerformer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TerminalPerformer")
.field("attributes", &self.attributes)
.field("hyperlink_id", &self.current_hyperlink_id)
.field("hyperlink_url", &self.current_hyperlink_url)
.finish_non_exhaustive()
}
}
impl TerminalPerformer {
pub fn new(screen: Arc<Mutex<Screen>>) -> Self {
Self {
screen,
attributes: ScreenCellAttributes::default(),
sgr_state: HashMap::new(),
current_hyperlink_id: None,
current_hyperlink_url: None,
osc_params: Vec::new(),
}
}
pub fn screen(&self) -> &Arc<Mutex<Screen>> {
&self.screen
}
fn reset_attributes(&mut self) {
self.attributes = ScreenCellAttributes::default();
self.sgr_state.clear();
}
fn reset_hyperlink(&mut self) {
self.current_hyperlink_id = None;
self.current_hyperlink_url = None;
self.attributes.style.set(CellStyle::HYPERLINK, false);
self.attributes.hyperlink_url = None;
}
fn track_sgr(&mut self, param: u16) {
self.sgr_state.insert(param, true);
}
fn untrack_sgr(&mut self, params: &[u16]) {
for param in params {
self.sgr_state.remove(param);
}
}
fn handle_sgr_params(&mut self, params: &vte::Params) {
if params.is_empty() {
self.reset_attributes();
return;
}
for param_values in params.iter().flatten() {
self.handle_sgr_param(*param_values);
}
}
fn handle_sgr_param(&mut self, param: u16) {
if self.handle_basic_sgr_style(param)
|| self.handle_font_sgr(param)
|| self.handle_color_sgr(param)
|| self.handle_frame_sgr(param)
|| self.handle_script_sgr(param)
{
return;
}
debug!("Unsupported SGR parameter: {}", param);
}
fn handle_basic_sgr_style(&mut self, param: u16) -> bool {
match param {
0 => self.reset_attributes(),
1 => self.attributes.style.set(CellStyle::BOLD, true),
2 => self.attributes.style.set(CellStyle::DIM, true),
3 => self.attributes.style.set(CellStyle::ITALIC, true),
4 => {
self.attributes.style.set(CellStyle::UNDERLINE, true);
self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
}
5 | 6 => self.attributes.style.set(CellStyle::BLINK, true),
7 => self.attributes.style.set(CellStyle::REVERSE, true),
8 => self.attributes.style.set(CellStyle::CONCEAL, true),
9 => self.attributes.style.set(CellStyle::STRIKETHROUGH, true),
20 => self.attributes.style.set(CellStyle::FRAKTUR, true),
21 => {
self.attributes.style.set(CellStyle::UNDERLINE, true);
self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, true);
}
22 => {
self.attributes.style.set(CellStyle::BOLD, false);
self.attributes.style.set(CellStyle::DIM, false);
self.untrack_sgr(&[1, 2]);
return true;
}
23 => {
self.attributes.style.set(CellStyle::ITALIC, false);
self.attributes.style.set(CellStyle::FRAKTUR, false);
self.untrack_sgr(&[3, 20]);
return true;
}
24 => {
self.attributes.style.set(CellStyle::UNDERLINE, false);
self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
self.untrack_sgr(&[4, 21]);
return true;
}
25 => {
self.attributes.style.set(CellStyle::BLINK, false);
self.untrack_sgr(&[5, 6]);
return true;
}
27 => self.attributes.style.set(CellStyle::REVERSE, false),
28 => self.attributes.style.set(CellStyle::CONCEAL, false),
29 => self.attributes.style.set(CellStyle::STRIKETHROUGH, false),
_ => return false,
}
self.track_sgr(param);
true
}
fn handle_font_sgr(&mut self, param: u16) -> bool {
match param {
10 => self.attributes.font = 0,
11..=19 => self.attributes.font = (param - 10) as u8,
_ => return false,
}
self.track_sgr(param);
true
}
fn handle_color_sgr(&mut self, param: u16) -> bool {
match param {
26 | 38 | 48 => {}
30..=37 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 30)),
39 => self.attributes.fg_color = None,
40..=47 => self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 40)),
49 => self.attributes.bg_color = None,
90..=97 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 90 + 8)),
100..=107 => {
self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 100 + 8));
}
_ => return false,
}
true
}
fn handle_frame_sgr(&mut self, param: u16) -> bool {
match param {
51 => {
self.attributes.style.set(CellStyle::FRAMED, true);
self.attributes.style.set(CellStyle::ENCIRCLED, false);
}
52 => {
self.attributes.style.set(CellStyle::FRAMED, false);
self.attributes.style.set(CellStyle::ENCIRCLED, true);
}
53 => self.attributes.style.set(CellStyle::OVERLINED, true),
54 => {
self.attributes.style.set(CellStyle::FRAMED, false);
self.attributes.style.set(CellStyle::ENCIRCLED, false);
self.untrack_sgr(&[51, 52]);
return true;
}
55 => {
self.attributes.style.set(CellStyle::OVERLINED, false);
self.untrack_sgr(&[53]);
return true;
}
60..=65 => {}
_ => return false,
}
self.track_sgr(param);
true
}
fn handle_script_sgr(&mut self, param: u16) -> bool {
match param {
73 => {
self.attributes.style.set(CellStyle::SUPERSCRIPT, true);
self.attributes.style.set(CellStyle::SUBSCRIPT, false);
}
74 => {
self.attributes.style.set(CellStyle::SUBSCRIPT, true);
self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
}
75 => {
self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
self.attributes.style.set(CellStyle::SUBSCRIPT, false);
self.untrack_sgr(&[73, 74]);
return true;
}
_ => return false,
}
self.track_sgr(param);
true
}
}
impl TerminalPerformer {
fn handle_sgr_dispatch(&mut self, params: &vte::Params) {
self.handle_sgr_params(params);
let param_arrays: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
if param_arrays.len() >= 3 {
let mut i = 0;
while i < param_arrays.len() {
if param_arrays[i].len() == 1 {
if param_arrays[i][0] == 38 && i + 2 < param_arrays.len() {
if param_arrays[i + 1].len() == 1
&& param_arrays[i + 1][0] == 5
&& param_arrays[i + 2].len() == 1
{
let color = param_arrays[i + 2][0] as u8;
self.attributes.fg_color = Some(TerminalColor::Color256(color));
i += 3;
continue;
} else if param_arrays[i + 1].len() == 1
&& param_arrays[i + 1][0] == 2
&& i + 4 < param_arrays.len()
&& param_arrays[i + 2].len() == 1
&& param_arrays[i + 3].len() == 1
&& param_arrays[i + 4].len() == 1
{
let r = param_arrays[i + 2][0] as u8;
let g = param_arrays[i + 3][0] as u8;
let b = param_arrays[i + 4][0] as u8;
self.attributes.fg_color = Some(TerminalColor::TrueColor { r, g, b });
i += 5;
continue;
}
} else if param_arrays[i][0] == 48 && i + 2 < param_arrays.len() {
if param_arrays[i + 1].len() == 1
&& param_arrays[i + 1][0] == 5
&& param_arrays[i + 2].len() == 1
{
let color = param_arrays[i + 2][0] as u8;
self.attributes.bg_color = Some(TerminalColor::Color256(color));
i += 3;
continue;
} else if param_arrays[i + 1].len() == 1
&& param_arrays[i + 1][0] == 2
&& i + 4 < param_arrays.len()
&& param_arrays[i + 2].len() == 1
&& param_arrays[i + 3].len() == 1
&& param_arrays[i + 4].len() == 1
{
let r = param_arrays[i + 2][0] as u8;
let g = param_arrays[i + 3][0] as u8;
let b = param_arrays[i + 4][0] as u8;
self.attributes.bg_color = Some(TerminalColor::TrueColor { r, g, b });
i += 5;
continue;
}
}
}
i += 1;
}
}
}
fn handle_osc_params(&mut self, params: &[&[u8]], _bell_terminated: bool) {
if params.is_empty() {
return;
}
let param_strings: Vec<String> =
params.iter().map(|p| String::from_utf8_lossy(p).to_string()).collect();
if param_strings.is_empty() {
return;
}
if param_strings[0] == "8" && param_strings.len() >= 3 {
let params =
if param_strings.len() > 1 { param_strings[1].clone() } else { String::new() };
let url =
if param_strings.len() > 2 { param_strings[2].clone() } else { String::new() };
let mut hyperlink_id = None;
for param in params.split(':') {
let parts: Vec<&str> = param.split('=').collect();
if parts.len() == 2 && parts[0] == "id" {
hyperlink_id = Some(parts[1].to_string());
}
}
if url.is_empty() {
self.reset_hyperlink();
} else {
self.attributes.style.set(CellStyle::HYPERLINK, true);
self.attributes.hyperlink_url = Some(url.clone());
self.current_hyperlink_url = Some(url);
if let Some(id) = hyperlink_id {
self.current_hyperlink_id = Some(id);
}
}
}
}
fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 {
params.iter().nth(index).and_then(|p| p.first().copied()).unwrap_or(default)
}
fn handle_cursor_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
let n = usize::from(Self::csi_param(params, 0, 1));
let current_row = screen.cursor_row();
let current_col = screen.cursor_col();
match c {
'A' => screen.move_cursor(current_row.saturating_sub(n), current_col),
'B' => screen.move_cursor(current_row + n, current_col),
'C' => screen.move_cursor(current_row, current_col + n),
'D' => screen.move_cursor(current_row, current_col.saturating_sub(n)),
'H' | 'f' => {
let row = usize::from(Self::csi_param(params, 0, 1)).saturating_sub(1);
let col = usize::from(Self::csi_param(params, 1, 1)).saturating_sub(1);
screen.move_cursor(row, col);
}
_ => return false,
}
true
}
fn clear_line_to_cursor(screen: &mut Screen) {
let row = screen.cursor_row();
let col = screen.cursor_col();
if row < screen.lines.len() {
for i in 0..=col.min(screen.lines[row].len().saturating_sub(1)) {
screen.lines[row][i] = ScreenCell::default();
}
}
}
fn handle_erase_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
match c {
'J' => Self::handle_erase_display(screen, Self::csi_param(params, 0, 0)),
'K' => Self::handle_erase_line(screen, Self::csi_param(params, 0, 0)),
_ => return false,
}
true
}
fn handle_erase_display(screen: &mut Screen, mode: u16) {
match mode {
0 => {
screen.clear_line_forward();
let row = screen.cursor_row();
if row + 1 < screen.lines.len() {
for i in row + 1..screen.lines.len() {
screen.lines[i] = vec![ScreenCell::default(); screen.columns];
}
}
}
1 => {
Self::clear_line_to_cursor(screen);
for i in 0..screen.cursor_row() {
if i < screen.lines.len() {
screen.lines[i] = vec![ScreenCell::default(); screen.columns];
}
}
}
2 | 3 => screen.clear(),
_ => debug!("Unhandled erase in display: {}", mode),
}
}
fn handle_erase_line(screen: &mut Screen, mode: u16) {
match mode {
0 => screen.clear_line_forward(),
1 => Self::clear_line_to_cursor(screen),
2 => screen.clear_line(),
_ => debug!("Unhandled erase in line: {}", mode),
}
}
fn handle_scroll_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
let n = usize::from(Self::csi_param(params, 0, 1));
match c {
'S' => {
for _ in 0..n {
screen.scroll_up();
}
}
'T' => {
let columns = screen.columns;
for _ in 0..n {
screen.lines.push_front(vec![ScreenCell::default(); columns]);
if screen.lines.len() > screen.max_lines {
screen.lines.pop_back();
}
}
screen.move_cursor(screen.cursor_row() + n, screen.cursor_col());
}
_ => return false,
}
true
}
}
impl Perform for TerminalPerformer {
fn print(&mut self, c: char) {
if let Ok(mut screen) = self.screen.lock() {
screen.put_char(c, self.attributes.clone());
} else {
warn!("Failed to lock screen for print");
}
}
fn execute(&mut self, byte: u8) {
if let Ok(mut screen) = self.screen.lock() {
match byte {
b'\r' => screen.carriage_return(),
b'\n' => {
screen.carriage_return();
screen.linefeed();
}
b'\t' => {
let current_col = screen.cursor_col();
let new_col = (current_col + 8) & !7;
let current_row = screen.cursor_row();
screen.move_cursor(current_row, new_col);
}
b'\x08' => {
if screen.cursor_col() > 0 {
let current_row = screen.cursor_row();
let new_col = screen.cursor_col() - 1;
screen.move_cursor(current_row, new_col);
}
}
b'\x0C' => {
screen.clear();
}
b'\x07' => { }
_ => {
debug!("Unhandled execute: {:?}", byte);
}
}
} else {
warn!("Failed to lock screen for execute");
}
}
fn hook(&mut self, _params: &vte::Params, _intermediates: &[u8], _ignore: bool, _c: char) {
}
fn put(&mut self, _byte: u8) {
}
fn unhook(&mut self) {
}
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
self.handle_osc_params(params, bell_terminated);
}
fn csi_dispatch(
&mut self,
params: &vte::Params,
_intermediates: &[u8],
_ignore: bool,
c: char,
) {
if c == 'm' {
self.handle_sgr_dispatch(params);
return;
}
if let Ok(mut screen) = self.screen.lock() {
if Self::handle_cursor_csi(&mut screen, params, c)
|| Self::handle_erase_csi(&mut screen, params, c)
|| Self::handle_scroll_csi(&mut screen, params, c)
{
return;
}
debug!("Unhandled CSI: {:?} {:?}", params, c);
} else {
warn!("Failed to lock screen for csi_dispatch");
}
}
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
if intermediates.is_empty() {
match byte {
b'c' => {
if let Ok(mut screen) = self.screen.lock() {
screen.clear();
}
self.reset_attributes();
}
b'7' | b'8' => {
}
_ => debug!("Unhandled ESC dispatch: {:?}", byte),
}
}
}
}
#[derive(Clone)]
pub struct TerminalEmulator {
performer: TerminalPerformer,
screen: Arc<Mutex<Screen>>,
}
impl std::fmt::Debug for TerminalEmulator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TerminalEmulator")
.field("performer", &self.performer)
.finish_non_exhaustive()
}
}
impl TerminalEmulator {
pub fn new(columns: usize) -> Self {
let screen = Arc::new(Mutex::new(Screen::new(columns)));
let performer = TerminalPerformer::new(screen.clone());
Self { performer, screen }
}
pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
let screen = Arc::new(Mutex::new(Screen::new_with_max_lines(columns, max_lines)));
let performer = TerminalPerformer::new(screen.clone());
Self { performer, screen }
}
pub fn process(&mut self, data: &str) {
let mut parser = Parser::new();
let chunk_size = 4096;
let data_bytes = data.as_bytes();
for chunk in data_bytes.chunks(chunk_size) {
parser.advance(&mut self.performer, chunk);
}
}
pub fn process_with_limited_buffer(&mut self, data: &str, max_lines: usize) {
if let Ok(mut screen) = self.screen.lock() {
screen.max_lines = max_lines.min(MAX_SCREEN_LINES);
}
self.process(data);
if let Ok(mut screen) = self.screen.lock() {
if screen.lines.len() > max_lines {
screen.smart_truncate(max_lines);
}
}
}
pub fn get_screen(&self) -> Arc<Mutex<Screen>> {
self.screen.clone()
}
pub fn display(&self) -> Vec<String> {
if let Ok(screen) = self.screen.lock() {
screen.display()
} else {
warn!("Failed to lock screen for display");
vec![]
}
}
pub fn to_plain_text(&self) -> String {
if let Ok(screen) = self.screen.lock() {
screen.to_plain_text()
} else {
warn!("Failed to lock screen for to_plain_text");
String::new()
}
}
pub fn clear(&mut self) {
if let Ok(mut screen) = self.screen.lock() {
screen.clear();
} else {
warn!("Failed to lock screen for clear");
}
}
}
type CacheEntryMap = HashMap<String, (Vec<String>, Instant)>;
#[derive(Debug, Clone)]
struct TerminalCache {
entries: Arc<RwLock<CacheEntryMap>>,
max_entries: usize,
ttl: u64,
}
impl TerminalCache {
fn new(max_entries: usize, ttl: u64) -> Self {
Self { entries: Arc::new(RwLock::new(HashMap::new())), max_entries, ttl }
}
fn get(&self, key: &str) -> Option<Vec<String>> {
if let Ok(entries) = self.entries.read() {
if let Some((value, timestamp)) = entries.get(key) {
if timestamp.elapsed().as_secs() < self.ttl {
return Some(value.clone());
}
}
}
None
}
fn insert(&self, key: String, value: Vec<String>) {
if let Ok(mut entries) = self.entries.write() {
entries.insert(key, (value, Instant::now()));
if entries.len() > self.max_entries {
entries.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < self.ttl);
if entries.len() > self.max_entries {
let mut entries_vec: Vec<_> = entries.iter().collect();
entries_vec.sort_by_key(|(_, (_, timestamp))| *timestamp);
let to_remove = entries_vec.len() - self.max_entries;
let keys_to_remove: Vec<String> =
entries_vec.iter().take(to_remove).map(|(k, _)| (*k).clone()).collect();
for key in keys_to_remove {
entries.remove(&key);
}
}
}
}
}
fn cleanup(&self) {
if let Ok(mut entries) = self.entries.write() {
entries.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < self.ttl);
}
}
}
lazy_static::lazy_static! {
static ref TERMINAL_CACHE: TerminalCache = TerminalCache::new(100, CACHE_TTL);
}
#[derive(Debug, Clone)]
pub struct TerminalOutputDiff {
previous_output: Vec<String>,
output_hash: String,
max_lines: usize,
}
impl Default for TerminalOutputDiff {
fn default() -> Self {
Self::new()
}
}
impl TerminalOutputDiff {
pub fn new() -> Self {
Self { previous_output: Vec::new(), output_hash: String::new(), max_lines: 1000 }
}
pub fn new_with_max_lines(max_lines: usize) -> Self {
Self { previous_output: Vec::new(), output_hash: String::new(), max_lines }
}
pub fn detect_changes(&mut self, new_output: &[String]) -> Vec<String> {
if self.previous_output.is_empty() {
self.previous_output = new_output.to_vec();
self.output_hash = self.calculate_hash(new_output);
return new_output.to_vec();
}
let new_hash = self.calculate_hash(new_output);
if new_hash == self.output_hash {
return Vec::new(); }
let mut changes = Vec::new();
let nold = self.previous_output.len().min(self.max_lines);
let nnew = new_output.len().min(self.max_lines);
let mut matched_position = None;
let is_prefix = nold <= nnew && (0..nold).all(|i| self.previous_output[i] == new_output[i]);
if is_prefix {
matched_position = Some(nold);
} else {
let mut best_match = 0;
let mut best_position = 0;
let window_size = 3.min(nold);
if window_size > 0 {
for i in (0..=nnew.saturating_sub(window_size)).rev() {
let mut match_count = 0;
for j in 0..window_size {
if i + j < nnew
&& nold.saturating_sub(window_size) + j < nold
&& new_output[i + j]
== self.previous_output[nold.saturating_sub(window_size) + j]
{
match_count += 1;
}
}
if match_count > best_match {
best_match = match_count;
best_position = i + window_size;
if best_match == window_size {
break;
}
}
}
}
if best_match >= window_size / 2 {
matched_position = Some(best_position);
}
}
if let Some(pos) = matched_position {
if pos < nnew {
changes = new_output[pos..].to_vec();
if !changes.is_empty()
&& !self.previous_output.is_empty()
&& changes[0] == self.previous_output[self.previous_output.len() - 1]
{
changes.remove(0);
}
}
} else {
changes = new_output.to_vec();
}
self.previous_output = new_output.to_vec();
self.output_hash = new_hash;
changes
}
fn calculate_hash(&self, lines: &[String]) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for line in lines.iter().take(self.max_lines) {
std::hash::Hash::hash(line, &mut hasher);
}
format!("{:x}", std::hash::Hasher::finish(&hasher))
}
pub fn reset(&mut self) {
self.previous_output.clear();
self.output_hash.clear();
}
}
pub fn render_terminal_output(text: &str) -> Vec<String> {
if let Some(cached) = TERMINAL_CACHE.get(text) {
return cached;
}
let mut terminal = TerminalEmulator::new(DEFAULT_COLUMNS);
if text.len() > MAX_OUTPUT_SIZE {
terminal.process_with_limited_buffer(text, DEFAULT_MAX_SCREEN_LINES);
} else {
terminal.process(text);
}
let result = terminal.display();
if text.len() < MAX_OUTPUT_SIZE {
TERMINAL_CACHE.insert(text.to_string(), result.clone());
}
if rand::random::<u32>() % 100 == 0 {
TERMINAL_CACHE.cleanup();
}
result.into_iter().map(|line| strip_ansi_codes(&line)).collect()
}
pub fn incremental_text(text: &str, last_pending_output: &str) -> String {
if text.is_empty() {
return String::new();
}
if last_pending_output.is_empty() {
let lines = render_terminal_output(text);
return lines.join("\n").trim().to_string();
}
let is_append = text.starts_with(last_pending_output);
if is_append && text.len() > last_pending_output.len() {
let new_part = &text[last_pending_output.len()..];
let context_len = 200.min(last_pending_output.len());
let full_context = if context_len > 0 {
let start_pos = last_pending_output.len() - context_len;
format!("{}{}", &last_pending_output[start_pos..], new_part)
} else {
new_part.to_string()
};
let previous_lines = render_terminal_output(last_pending_output);
let combined_lines = render_terminal_output(&full_context);
let mut diff_detector = TerminalOutputDiff::new();
diff_detector.previous_output = previous_lines;
let changes = diff_detector.detect_changes(&combined_lines);
if changes.is_empty() {
return String::new();
}
return changes.join("\n");
}
let text_limit = if text.len() > MAX_OUTPUT_SIZE {
let start_offset = text.len() - MAX_OUTPUT_SIZE;
let adjusted_offset =
text[start_offset..].find('\n').map_or(start_offset, |pos| start_offset + pos + 1);
&text[adjusted_offset..]
} else {
text
};
let previous_lines = render_terminal_output(last_pending_output);
let new_lines = render_terminal_output(text_limit);
let mut diff_detector = TerminalOutputDiff::new();
diff_detector.previous_output = previous_lines;
let changes = diff_detector.detect_changes(&new_lines);
if changes.is_empty() {
return String::new();
}
changes.join("\n")
}
pub fn strip_ansi_codes(input: &str) -> String {
let pattern = r"[\u001b\u009b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]";
match Regex::new(pattern) {
Ok(re) => re.replace_all(input, "").to_string(),
Err(_) => input.replace('\x1b', ""), }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_screen_basic_operations() {
let mut screen = Screen::new(80);
let _attributes = ScreenCellAttributes::default();
screen.put_char_basic('H', CellStyle::default(), None, None);
screen.put_char_basic('e', CellStyle::default(), None, None);
screen.put_char_basic('l', CellStyle::default(), None, None);
screen.put_char_basic('l', CellStyle::default(), None, None);
screen.put_char_basic('o', CellStyle::default(), None, None);
let display = screen.display();
assert_eq!(display, vec!["Hello"]);
screen.carriage_return();
screen.linefeed();
screen.put_char_basic('W', CellStyle::default(), None, None);
screen.put_char_basic('o', CellStyle::default(), None, None);
screen.put_char_basic('r', CellStyle::default(), None, None);
screen.put_char_basic('l', CellStyle::default(), None, None);
screen.put_char_basic('d', CellStyle::default(), None, None);
let display = screen.display();
assert_eq!(display, vec!["Hello", "World"]);
screen.clear_line();
let display = screen.display();
assert_eq!(display, vec!["Hello"]);
}
#[test]
fn test_terminal_emulator_basic() {
let mut terminal = TerminalEmulator::new(80);
terminal.process("Hello\r\nWorld");
let display = terminal.display();
assert_eq!(display, vec!["Hello", "World"]);
terminal.clear();
terminal.process("Normal \x1b[1mBold\x1b[0m Normal");
let display = terminal.display();
assert_eq!(display, vec!["Normal Bold Normal"]);
terminal.clear();
terminal.process("Hello\x1b[5D_\x1b[1C_\x1b[1C_");
let display = terminal.display();
assert_eq!(display, vec!["_e_l_"]);
}
#[test]
fn test_incremental_output() {
let old = vec!["Line 1".to_string(), "Line 2".to_string()];
let new = vec!["Line 1".to_string(), "Line 2".to_string(), "Line 3".to_string()];
let mut diff_detector = TerminalOutputDiff::new();
diff_detector.previous_output = old;
let incremental = diff_detector.detect_changes(&new);
assert_eq!(incremental, vec!["Line 3"]);
let old = vec!["Line A".to_string(), "Line B".to_string()];
let new = vec!["Line X".to_string(), "Line Y".to_string()];
let mut diff_detector = TerminalOutputDiff::new();
diff_detector.previous_output = old;
let incremental = diff_detector.detect_changes(&new);
assert_eq!(incremental, vec!["Line X", "Line Y"]);
}
#[test]
fn test_render_terminal_output() {
let text = "Hello\r\nWorld\r\n\x1b[31mRed\x1b[0m Text";
let lines = render_terminal_output(text);
assert_eq!(lines, vec!["Hello", "World", "Red Text"]);
}
#[test]
fn test_smart_truncate() {
let mut screen = Screen::new_with_max_lines(80, 20);
for i in 0..30 {
let line = format!("Line {i}");
for c in line.chars() {
screen.put_char(c, ScreenCellAttributes::default());
}
screen.carriage_return();
screen.linefeed();
}
assert_eq!(screen.lines.len(), 20);
screen.smart_truncate(10);
assert_eq!(screen.lines.len(), 10);
let has_truncation_marker = screen.lines.iter().any(|line| {
let line_text: String = line.iter().map(|cell| cell.character).collect();
line_text.contains("TRUNCATED")
});
assert!(has_truncation_marker);
}
#[test]
fn test_terminal_cache() {
let cache = TerminalCache::new(10, 60);
cache.insert("test".to_string(), vec!["line1".to_string(), "line2".to_string()]);
let retrieved = cache.get("test");
assert_eq!(retrieved, Some(vec!["line1".to_string(), "line2".to_string()]));
let not_found = cache.get("unknown");
assert_eq!(not_found, None);
}
#[test]
fn test_incremental_text_append() {
let old_text = "Line 1\nLine 2\n";
let new_text = "Line 1\nLine 2\nLine 3\n";
let incremental = incremental_text(new_text, old_text);
assert_eq!(incremental, "Line 3");
}
#[test]
fn test_terminal_color_handling() {
let mut terminal = TerminalEmulator::new(80);
terminal.process("\x1b[31mRed\x1b[32mGreen\x1b[0mNormal");
let display = terminal.display();
assert_eq!(display, vec!["RedGreenNormal"]);
terminal.clear();
terminal.process("\x1b[38;5;208mOrange\x1b[0mNormal");
let display = terminal.display();
assert_eq!(display, vec!["OrangeNormal"]);
}
}