commit
This commit is contained in:
12
README.md
12
README.md
@@ -8,6 +8,18 @@ Claude Code uses synchronized output to update the terminal atomically. It wraps
|
|||||||
|
|
||||||
The problem: Claude Code sends *entire* screen redraws in these sync blocks - often thousands of lines. Your terminal receives a 5000-line atomic update when only 20 lines are visible. This causes lag, flicker, and makes scrollback useless since each update clears history.
|
The problem: Claude Code sends *entire* screen redraws in these sync blocks - often thousands of lines. Your terminal receives a 5000-line atomic update when only 20 lines are visible. This causes lag, flicker, and makes scrollback useless since each update clears history.
|
||||||
|
|
||||||
|
Analysis of terminal recordings confirms Claude Code wraps 100% of its output in sync blocks - every byte of visible output goes through synchronized updates.
|
||||||
|
|
||||||
|
Sync blocks start with one of three patterns (from a 3.5GB recording sample):
|
||||||
|
|
||||||
|
| Pattern | Count | Frequency | Avg Size |
|
||||||
|
|---------|-------|-----------|----------|
|
||||||
|
| Line clearing (`2K` + `1A` repeated) | 3,544 | 55% | 2.7 KB |
|
||||||
|
| Full screen clear (`2J` + `3J` + `H`) | 2,891 | 45% | 94.5 KB |
|
||||||
|
| CRLF + color codes | 1 | <1% | 2.0 KB |
|
||||||
|
|
||||||
|
The full screen clears are 35x larger than incremental line clears - these are the real problem.
|
||||||
|
|
||||||
## The Solution
|
## The Solution
|
||||||
|
|
||||||
claude-chill sits between your terminal and Claude Code:
|
claude-chill sits between your terminal and Claude Code:
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,5 @@ pub const CLEAR_SCROLLBACK: &[u8] = b"\x1b[3J";
|
|||||||
pub const CURSOR_HOME: &[u8] = b"\x1b[H";
|
pub const CURSOR_HOME: &[u8] = b"\x1b[H";
|
||||||
|
|
||||||
pub const SYNC_BUFFER_CAPACITY: usize = 1024 * 1024;
|
pub const SYNC_BUFFER_CAPACITY: usize = 1024 * 1024;
|
||||||
pub const PASSTHROUGH_BUFFER_CAPACITY: usize = 65536;
|
|
||||||
pub const OUTPUT_BUFFER_CAPACITY: usize = 32768;
|
pub const OUTPUT_BUFFER_CAPACITY: usize = 32768;
|
||||||
pub const PENDING_ESCAPE_CAPACITY: usize = 32;
|
|
||||||
pub const INPUT_BUFFER_CAPACITY: usize = 64;
|
pub const INPUT_BUFFER_CAPACITY: usize = 64;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod escape_parser;
|
|
||||||
pub mod escape_sequences;
|
pub mod escape_sequences;
|
||||||
pub mod key_parser;
|
pub mod key_parser;
|
||||||
pub mod line_buffer;
|
pub mod line_buffer;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::escape_parser::{EscapeParser, ParsedEscape};
|
|
||||||
use crate::escape_sequences::{
|
use crate::escape_sequences::{
|
||||||
CLEAR_SCREEN, CLEAR_SCROLLBACK, CURSOR_HOME, INPUT_BUFFER_CAPACITY, OUTPUT_BUFFER_CAPACITY,
|
CLEAR_SCREEN, CLEAR_SCROLLBACK, CURSOR_HOME, INPUT_BUFFER_CAPACITY, OUTPUT_BUFFER_CAPACITY,
|
||||||
PASSTHROUGH_BUFFER_CAPACITY, PENDING_ESCAPE_CAPACITY, SYNC_BUFFER_CAPACITY, SYNC_END,
|
SYNC_BUFFER_CAPACITY, SYNC_END, SYNC_START,
|
||||||
SYNC_START,
|
|
||||||
};
|
};
|
||||||
use crate::line_buffer::LineBuffer;
|
use crate::line_buffer::LineBuffer;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@@ -59,11 +57,10 @@ pub struct Proxy {
|
|||||||
history: LineBuffer,
|
history: LineBuffer,
|
||||||
sync_buffer: Vec<u8>,
|
sync_buffer: Vec<u8>,
|
||||||
in_sync_block: bool,
|
in_sync_block: bool,
|
||||||
parser: EscapeParser,
|
|
||||||
input_buffer: Vec<u8>,
|
input_buffer: Vec<u8>,
|
||||||
passthrough_buffer: Vec<u8>,
|
|
||||||
output_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>,
|
clear_screen_finder: memmem::Finder<'static>,
|
||||||
cursor_home_finder: memmem::Finder<'static>,
|
cursor_home_finder: memmem::Finder<'static>,
|
||||||
}
|
}
|
||||||
@@ -117,11 +114,10 @@ impl Proxy {
|
|||||||
original_termios,
|
original_termios,
|
||||||
sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY),
|
sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY),
|
||||||
in_sync_block: false,
|
in_sync_block: false,
|
||||||
parser: EscapeParser::new(),
|
|
||||||
input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
||||||
passthrough_buffer: Vec::with_capacity(PASSTHROUGH_BUFFER_CAPACITY),
|
|
||||||
output_buffer: Vec::with_capacity(OUTPUT_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),
|
clear_screen_finder: memmem::Finder::new(CLEAR_SCREEN),
|
||||||
cursor_home_finder: memmem::Finder::new(CURSOR_HOME),
|
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<()> {
|
fn process_output(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
|
||||||
self.passthrough_buffer.clear();
|
let mut pos = 0;
|
||||||
|
|
||||||
for &byte in data {
|
while pos < data.len() {
|
||||||
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 {
|
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.sync_buffer.extend_from_slice(SYNC_END);
|
||||||
self.flush_sync_block(stdout_fd)?;
|
self.flush_sync_block(stdout_fd)?;
|
||||||
self.in_sync_block = false;
|
self.in_sync_block = false;
|
||||||
}
|
pos += idx + SYNC_END.len();
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.flush_pending_escape();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
self.passthrough_buffer.push(byte);
|
self.sync_buffer.extend_from_slice(&data[pos..]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
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(())
|
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<()> {
|
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_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();
|
let has_cursor_home = self.cursor_home_finder.find(&self.sync_buffer).is_some();
|
||||||
@@ -278,13 +235,10 @@ impl Proxy {
|
|||||||
|
|
||||||
if is_full_redraw {
|
if is_full_redraw {
|
||||||
self.history.clear();
|
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();
|
self.create_truncated_output();
|
||||||
write_all_raw(stdout_fd, &self.output_buffer)?;
|
write_all_raw(stdout_fd, &self.output_buffer)?;
|
||||||
} else {
|
} else {
|
||||||
@@ -305,28 +259,6 @@ impl Proxy {
|
|||||||
self.output_buffer.extend_from_slice(SYNC_END);
|
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<()> {
|
fn process_input(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
|
||||||
let master_fd = self.pty_master.as_raw_fd();
|
let master_fd = self.pty_master.as_raw_fd();
|
||||||
for &byte in data {
|
for &byte in data {
|
||||||
|
|||||||
Reference in New Issue
Block a user