This commit is contained in:
Dave Beesley
2026-01-16 22:50:11 -07:00
parent 961fc90313
commit 58f448e05e
5 changed files with 42 additions and 620 deletions

View File

@@ -1,519 +0,0 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParsedEscape {
SyncStart,
SyncEnd,
ClearScreen,
ClearScrollback,
CursorHome,
CursorUp(u16),
CursorCol(u16),
ClearLine,
Newline,
CarriageReturn,
Sgr(SgrCode),
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SgrCode {
pub reset: bool,
pub fg: Option<Color>,
pub bg: Option<Color>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Default,
Indexed(u8),
Rgb(u8, u8, u8),
}
pub struct EscapeParser {
state: ParserState,
params: Vec<u16>,
intermediate: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParserState {
Ground,
Escape,
CsiEntry,
CsiParam,
CsiIntermediate,
OscString,
DcsString,
}
impl Default for EscapeParser {
fn default() -> Self {
Self::new()
}
}
impl EscapeParser {
pub fn new() -> Self {
Self {
state: ParserState::Ground,
params: Vec::with_capacity(16),
intermediate: Vec::with_capacity(4),
}
}
pub fn in_escape_sequence(&self) -> bool {
self.state != ParserState::Ground
}
pub fn feed(&mut self, byte: u8) -> Option<ParsedEscape> {
match self.state {
ParserState::Ground => self.ground(byte),
ParserState::Escape => self.escape(byte),
ParserState::CsiEntry => self.csi_entry(byte),
ParserState::CsiParam => self.csi_param(byte),
ParserState::CsiIntermediate => self.csi_intermediate(byte),
ParserState::OscString => self.osc_string(byte),
ParserState::DcsString => self.dcs_string(byte),
}
}
fn ground(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
0x1b => {
self.state = ParserState::Escape;
None
}
b'\n' => Some(ParsedEscape::Newline),
b'\r' => Some(ParsedEscape::CarriageReturn),
_ => None,
}
}
fn escape(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
b'[' => {
self.state = ParserState::CsiEntry;
self.params.clear();
self.intermediate.clear();
None
}
b']' => {
self.state = ParserState::OscString;
None
}
b'P' | b'^' | b'_' => {
self.state = ParserState::DcsString;
None
}
_ => {
self.state = ParserState::Ground;
Some(ParsedEscape::Other)
}
}
}
fn osc_string(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
0x07 => {
self.state = ParserState::Ground;
Some(ParsedEscape::Other)
}
0x1b => {
self.state = ParserState::Escape;
None
}
_ => None,
}
}
fn dcs_string(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
0x1b => {
self.state = ParserState::Escape;
None
}
0x9c => {
self.state = ParserState::Ground;
Some(ParsedEscape::Other)
}
_ => None,
}
}
fn csi_entry(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
b'0'..=b'9' => {
self.params.push((byte - b'0') as u16);
self.state = ParserState::CsiParam;
None
}
b';' => {
self.params.push(0);
self.state = ParserState::CsiParam;
None
}
b'?' => {
self.intermediate.push(byte);
self.state = ParserState::CsiIntermediate;
None
}
b'@'..=b'~' => {
self.state = ParserState::Ground;
self.dispatch_csi(byte)
}
_ => {
self.state = ParserState::Ground;
Some(ParsedEscape::Other)
}
}
}
fn csi_param(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
b'0'..=b'9' => {
if let Some(last) = self.params.last_mut() {
*last = last.saturating_mul(10).saturating_add((byte - b'0') as u16);
}
None
}
b';' => {
self.params.push(0);
None
}
b'@'..=b'~' => {
self.state = ParserState::Ground;
self.dispatch_csi(byte)
}
_ => {
self.state = ParserState::Ground;
Some(ParsedEscape::Other)
}
}
}
fn csi_intermediate(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
b'0'..=b'9' => {
if self.params.is_empty() {
self.params.push((byte - b'0') as u16);
} else if let Some(last) = self.params.last_mut() {
*last = last.saturating_mul(10).saturating_add((byte - b'0') as u16);
}
None
}
b';' => {
self.params.push(0);
None
}
b'@'..=b'~' => {
self.state = ParserState::Ground;
self.dispatch_private_csi(byte)
}
_ => {
self.intermediate.push(byte);
None
}
}
}
fn dispatch_csi(&mut self, byte: u8) -> Option<ParsedEscape> {
match byte {
b'H' => {
if self.params.is_empty()
|| (self.params.len() == 2 && self.params[0] <= 1 && self.params[1] <= 1)
{
Some(ParsedEscape::CursorHome)
} else {
Some(ParsedEscape::Other)
}
}
b'J' => {
let param = self.params.first().copied().unwrap_or(0);
match param {
2 => Some(ParsedEscape::ClearScreen),
3 => Some(ParsedEscape::ClearScrollback),
_ => Some(ParsedEscape::Other),
}
}
b'A' => {
let n = self.params.first().copied().unwrap_or(1).max(1);
Some(ParsedEscape::CursorUp(n))
}
b'G' => {
let col = self.params.first().copied().unwrap_or(1);
Some(ParsedEscape::CursorCol(col))
}
b'K' => Some(ParsedEscape::ClearLine),
b'm' => Some(ParsedEscape::Sgr(self.parse_sgr())),
_ => Some(ParsedEscape::Other),
}
}
fn dispatch_private_csi(&mut self, byte: u8) -> Option<ParsedEscape> {
if self.intermediate.first() == Some(&b'?') {
let param = self.params.first().copied().unwrap_or(0);
match (param, byte) {
(2026, b'h') => Some(ParsedEscape::SyncStart),
(2026, b'l') => Some(ParsedEscape::SyncEnd),
_ => Some(ParsedEscape::Other),
}
} else {
Some(ParsedEscape::Other)
}
}
fn parse_sgr(&self) -> SgrCode {
let mut sgr = SgrCode::default();
let mut i = 0;
while i < self.params.len() {
match self.params[i] {
0 => sgr.reset = true,
38 => {
if i + 1 < self.params.len() && self.params[i + 1] == 2 {
if i + 4 < self.params.len() {
let r = self.params[i + 2] as u8;
let g = self.params[i + 3] as u8;
let b = self.params[i + 4] as u8;
sgr.fg = Some(Color::Rgb(r, g, b));
i += 4;
}
} else if i + 1 < self.params.len()
&& self.params[i + 1] == 5
&& i + 2 < self.params.len()
{
sgr.fg = Some(Color::Indexed(self.params[i + 2] as u8));
i += 2;
}
}
48 => {
if i + 1 < self.params.len() && self.params[i + 1] == 2 {
if i + 4 < self.params.len() {
let r = self.params[i + 2] as u8;
let g = self.params[i + 3] as u8;
let b = self.params[i + 4] as u8;
sgr.bg = Some(Color::Rgb(r, g, b));
i += 4;
}
} else if i + 1 < self.params.len()
&& self.params[i + 1] == 5
&& i + 2 < self.params.len()
{
sgr.bg = Some(Color::Indexed(self.params[i + 2] as u8));
i += 2;
}
}
39 => sgr.fg = Some(Color::Default),
49 => sgr.bg = Some(Color::Default),
_ => {}
}
i += 1;
}
sgr
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_sequence(bytes: &[u8]) -> Vec<ParsedEscape> {
let mut parser = EscapeParser::new();
bytes.iter().filter_map(|&b| parser.feed(b)).collect()
}
fn parse_last(bytes: &[u8]) -> Option<ParsedEscape> {
parse_sequence(bytes).into_iter().last()
}
#[test]
fn test_newline() {
assert_eq!(parse_last(b"\n"), Some(ParsedEscape::Newline));
}
#[test]
fn test_carriage_return() {
assert_eq!(parse_last(b"\r"), Some(ParsedEscape::CarriageReturn));
}
#[test]
fn test_clear_screen() {
assert_eq!(parse_last(b"\x1b[2J"), Some(ParsedEscape::ClearScreen));
}
#[test]
fn test_clear_scrollback() {
assert_eq!(parse_last(b"\x1b[3J"), Some(ParsedEscape::ClearScrollback));
}
#[test]
fn test_cursor_home() {
assert_eq!(parse_last(b"\x1b[H"), Some(ParsedEscape::CursorHome));
assert_eq!(parse_last(b"\x1b[1;1H"), Some(ParsedEscape::CursorHome));
}
#[test]
fn test_cursor_home_with_position() {
assert_eq!(parse_last(b"\x1b[5;10H"), Some(ParsedEscape::Other));
}
#[test]
fn test_cursor_up() {
assert_eq!(parse_last(b"\x1b[A"), Some(ParsedEscape::CursorUp(1)));
assert_eq!(parse_last(b"\x1b[5A"), Some(ParsedEscape::CursorUp(5)));
}
#[test]
fn test_cursor_col() {
assert_eq!(parse_last(b"\x1b[G"), Some(ParsedEscape::CursorCol(1)));
assert_eq!(parse_last(b"\x1b[15G"), Some(ParsedEscape::CursorCol(15)));
}
#[test]
fn test_clear_line() {
assert_eq!(parse_last(b"\x1b[K"), Some(ParsedEscape::ClearLine));
}
#[test]
fn test_sync_start() {
assert_eq!(parse_last(b"\x1b[?2026h"), Some(ParsedEscape::SyncStart));
}
#[test]
fn test_sync_end() {
assert_eq!(parse_last(b"\x1b[?2026l"), Some(ParsedEscape::SyncEnd));
}
#[test]
fn test_sgr_reset() {
let result = parse_last(b"\x1b[0m");
assert_eq!(
result,
Some(ParsedEscape::Sgr(SgrCode {
reset: true,
fg: None,
bg: None,
}))
);
}
#[test]
fn test_sgr_default_colors() {
let result = parse_last(b"\x1b[39;49m");
assert_eq!(
result,
Some(ParsedEscape::Sgr(SgrCode {
reset: false,
fg: Some(Color::Default),
bg: Some(Color::Default),
}))
);
}
#[test]
fn test_sgr_indexed_fg() {
let result = parse_last(b"\x1b[38;5;196m");
assert_eq!(
result,
Some(ParsedEscape::Sgr(SgrCode {
reset: false,
fg: Some(Color::Indexed(196)),
bg: None,
}))
);
}
#[test]
fn test_sgr_indexed_bg() {
let result = parse_last(b"\x1b[48;5;21m");
assert_eq!(
result,
Some(ParsedEscape::Sgr(SgrCode {
reset: false,
fg: None,
bg: Some(Color::Indexed(21)),
}))
);
}
#[test]
fn test_sgr_rgb_fg() {
let result = parse_last(b"\x1b[38;2;255;128;0m");
assert_eq!(
result,
Some(ParsedEscape::Sgr(SgrCode {
reset: false,
fg: Some(Color::Rgb(255, 128, 0)),
bg: None,
}))
);
}
#[test]
fn test_sgr_rgb_bg() {
let result = parse_last(b"\x1b[48;2;0;128;255m");
assert_eq!(
result,
Some(ParsedEscape::Sgr(SgrCode {
reset: false,
fg: None,
bg: Some(Color::Rgb(0, 128, 255)),
}))
);
}
#[test]
fn test_osc_title_bel_terminated() {
let events = parse_sequence(b"\x1b]0;My Title\x07");
assert_eq!(events, vec![ParsedEscape::Other]);
}
#[test]
fn test_osc_title_st_terminated() {
let events = parse_sequence(b"\x1b]0;My Title\x1b\\");
assert_eq!(events, vec![ParsedEscape::Other]);
}
#[test]
fn test_dcs_sequence() {
let events = parse_sequence(b"\x1bPsome data\x1b\\");
assert_eq!(events, vec![ParsedEscape::Other]);
}
#[test]
fn test_apc_sequence() {
let events = parse_sequence(b"\x1b_application data\x1b\\");
assert_eq!(events, vec![ParsedEscape::Other]);
}
#[test]
fn test_pm_sequence() {
let events = parse_sequence(b"\x1b^private message\x1b\\");
assert_eq!(events, vec![ParsedEscape::Other]);
}
#[test]
fn test_mixed_content() {
let events = parse_sequence(b"hello\nworld\r\n");
assert_eq!(
events,
vec![
ParsedEscape::Newline,
ParsedEscape::CarriageReturn,
ParsedEscape::Newline
]
);
}
#[test]
fn test_escape_followed_by_text() {
let events = parse_sequence(b"\x1b[2Jhello\n");
assert_eq!(
events,
vec![ParsedEscape::ClearScreen, ParsedEscape::Newline]
);
}
#[test]
fn test_unknown_csi() {
assert_eq!(parse_last(b"\x1b[999z"), Some(ParsedEscape::Other));
}
}

View File

@@ -5,7 +5,5 @@ pub const CLEAR_SCROLLBACK: &[u8] = b"\x1b[3J";
pub const CURSOR_HOME: &[u8] = b"\x1b[H";
pub const SYNC_BUFFER_CAPACITY: usize = 1024 * 1024;
pub const PASSTHROUGH_BUFFER_CAPACITY: usize = 65536;
pub const OUTPUT_BUFFER_CAPACITY: usize = 32768;
pub const PENDING_ESCAPE_CAPACITY: usize = 32;
pub const INPUT_BUFFER_CAPACITY: usize = 64;

View File

@@ -1,5 +1,4 @@
pub mod config;
pub mod escape_parser;
pub mod escape_sequences;
pub mod key_parser;
pub mod line_buffer;

View File

@@ -1,8 +1,6 @@
use crate::escape_parser::{EscapeParser, ParsedEscape};
use crate::escape_sequences::{
CLEAR_SCREEN, CLEAR_SCROLLBACK, CURSOR_HOME, INPUT_BUFFER_CAPACITY, OUTPUT_BUFFER_CAPACITY,
PASSTHROUGH_BUFFER_CAPACITY, PENDING_ESCAPE_CAPACITY, SYNC_BUFFER_CAPACITY, SYNC_END,
SYNC_START,
SYNC_BUFFER_CAPACITY, SYNC_END, SYNC_START,
};
use crate::line_buffer::LineBuffer;
use anyhow::{Context, Result};
@@ -59,11 +57,10 @@ pub struct Proxy {
history: LineBuffer,
sync_buffer: Vec<u8>,
in_sync_block: bool,
parser: EscapeParser,
input_buffer: Vec<u8>,
passthrough_buffer: Vec<u8>,
output_buffer: Vec<u8>,
pending_escape: Vec<u8>,
sync_start_finder: memmem::Finder<'static>,
sync_end_finder: memmem::Finder<'static>,
clear_screen_finder: memmem::Finder<'static>,
cursor_home_finder: memmem::Finder<'static>,
}
@@ -117,11 +114,10 @@ impl Proxy {
original_termios,
sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY),
in_sync_block: false,
parser: EscapeParser::new(),
input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
passthrough_buffer: Vec::with_capacity(PASSTHROUGH_BUFFER_CAPACITY),
output_buffer: Vec::with_capacity(OUTPUT_BUFFER_CAPACITY),
pending_escape: Vec::with_capacity(PENDING_ESCAPE_CAPACITY),
sync_start_finder: memmem::Finder::new(SYNC_START),
sync_end_finder: memmem::Finder::new(SYNC_END),
clear_screen_finder: memmem::Finder::new(CLEAR_SCREEN),
cursor_home_finder: memmem::Finder::new(CURSOR_HOME),
})
@@ -201,76 +197,37 @@ impl Proxy {
}
fn process_output(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
self.passthrough_buffer.clear();
let mut pos = 0;
for &byte in data {
let in_escape = self.parser.in_escape_sequence();
if in_escape && self.pending_escape.is_empty() {
self.pending_escape.push(0x1b);
}
if let Some(event) = self.parser.feed(byte) {
match event {
ParsedEscape::SyncStart => {
if !self.passthrough_buffer.is_empty() {
write_all_raw(stdout_fd, &self.passthrough_buffer)?;
self.passthrough_buffer.clear();
}
self.pending_escape.clear();
self.in_sync_block = true;
self.sync_buffer.clear();
self.sync_buffer.extend_from_slice(SYNC_START);
continue;
}
ParsedEscape::SyncEnd => {
self.pending_escape.clear();
if self.in_sync_block {
self.sync_buffer.extend_from_slice(SYNC_END);
self.flush_sync_block(stdout_fd)?;
self.in_sync_block = false;
}
continue;
}
_ => {
self.flush_pending_escape();
}
while pos < data.len() {
if self.in_sync_block {
if let Some(idx) = self.sync_end_finder.find(&data[pos..]) {
self.sync_buffer.extend_from_slice(&data[pos..pos + idx]);
self.sync_buffer.extend_from_slice(SYNC_END);
self.flush_sync_block(stdout_fd)?;
self.in_sync_block = false;
pos += idx + SYNC_END.len();
} else {
self.sync_buffer.extend_from_slice(&data[pos..]);
break;
}
}
if !self.parser.in_escape_sequence() && !self.pending_escape.is_empty() {
self.flush_pending_escape();
}
if self.parser.in_escape_sequence() {
self.pending_escape.push(byte);
} else if self.in_sync_block {
self.sync_buffer.push(byte);
} else if let Some(idx) = self.sync_start_finder.find(&data[pos..]) {
if idx > 0 {
write_all_raw(stdout_fd, &data[pos..pos + idx])?;
}
self.in_sync_block = true;
self.sync_buffer.clear();
self.sync_buffer.extend_from_slice(SYNC_START);
pos += idx + SYNC_START.len();
} else {
self.passthrough_buffer.push(byte);
write_all_raw(stdout_fd, &data[pos..])?;
break;
}
}
if !self.pending_escape.is_empty() && !self.parser.in_escape_sequence() {
self.flush_pending_escape();
}
if !self.passthrough_buffer.is_empty() {
write_all_raw(stdout_fd, &self.passthrough_buffer)?;
}
Ok(())
}
fn flush_pending_escape(&mut self) {
if self.in_sync_block {
self.sync_buffer.extend_from_slice(&self.pending_escape);
} else {
self.passthrough_buffer
.extend_from_slice(&self.pending_escape);
}
self.pending_escape.clear();
}
fn flush_sync_block(&mut self, stdout_fd: i32) -> Result<()> {
let has_clear_screen = self.clear_screen_finder.find(&self.sync_buffer).is_some();
let has_cursor_home = self.cursor_home_finder.find(&self.sync_buffer).is_some();
@@ -278,13 +235,10 @@ impl Proxy {
if is_full_redraw {
self.history.clear();
let content_start = self.find_content_start();
let content_end = self.sync_buffer.len().saturating_sub(SYNC_END.len());
if content_start < content_end {
self.history
.push_bytes(&self.sync_buffer[content_start..content_end]);
}
}
self.history.push_bytes(&self.sync_buffer);
if is_full_redraw {
self.create_truncated_output();
write_all_raw(stdout_fd, &self.output_buffer)?;
} else {
@@ -305,28 +259,6 @@ impl Proxy {
self.output_buffer.extend_from_slice(SYNC_END);
}
fn find_content_start(&self) -> usize {
let sync_start_finder = memmem::Finder::new(SYNC_START);
let clear_screen_finder = memmem::Finder::new(CLEAR_SCREEN);
let clear_scrollback_finder = memmem::Finder::new(CLEAR_SCROLLBACK);
let cursor_home_finder = memmem::Finder::new(CURSOR_HOME);
let mut pos = 0;
if let Some(idx) = sync_start_finder.find(&self.sync_buffer[pos..]) {
pos += idx + SYNC_START.len();
}
if let Some(idx) = clear_screen_finder.find(&self.sync_buffer[pos..]) {
pos += idx + CLEAR_SCREEN.len();
}
if let Some(idx) = clear_scrollback_finder.find(&self.sync_buffer[pos..]) {
pos += idx + CLEAR_SCROLLBACK.len();
}
if let Some(idx) = cursor_home_finder.find(&self.sync_buffer[pos..]) {
pos += idx + CURSOR_HOME.len();
}
pos
}
fn process_input(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
let master_fd = self.pty_master.as_raw_fd();
for &byte in data {