esc handling progress

This commit is contained in:
Zacharias-Brohn
2026-02-02 18:27:29 +01:00
parent 32fa9c891b
commit dbacb4b7ae
5 changed files with 764 additions and 361 deletions
+63 -6
View File
@@ -381,10 +381,29 @@ fn vs_cell_bg(
let col = instance_index % grid_params.cols; let col = instance_index % grid_params.cols;
let row = instance_index / grid_params.cols; let row = instance_index / grid_params.cols;
// Skip if out of bounds // Skip if out of bounds - place vertex outside clip volume (z=2 is beyond far plane)
if row >= grid_params.rows { if row >= grid_params.rows {
var out: CellVertexOutput; var out: CellVertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0); out.clip_position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
out.uv = vec2<f32>(0.0, 0.0);
out.fg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.bg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.is_background = 1u;
out.is_colored_glyph = 0u;
out.is_cursor = 0u;
out.cursor_shape = 0u;
out.cursor_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.cursor_uv = vec2<f32>(0.0, 0.0);
out.cell_size = vec2<f32>(0.0, 0.0);
out.underline_uv = vec2<f32>(0.0, 0.0);
out.strike_uv = vec2<f32>(0.0, 0.0);
out.decoration_fg = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.has_underline = 0u;
out.has_strikethrough = 0u;
out.glyph_layer = 0;
out.cursor_layer = 0;
out.underline_layer = 0;
out.strike_layer = 0;
return out; return out;
} }
@@ -517,10 +536,29 @@ fn vs_cell_glyph(
let col = instance_index % grid_params.cols; let col = instance_index % grid_params.cols;
let row = instance_index / grid_params.cols; let row = instance_index / grid_params.cols;
// Skip if out of bounds // Skip if out of bounds - use off-screen position with valid W to avoid undefined behavior
if row >= grid_params.rows { if row >= grid_params.rows {
var out: CellVertexOutput; var out: CellVertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0); out.clip_position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
out.uv = vec2<f32>(0.0, 0.0);
out.fg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.bg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.is_background = 0u;
out.is_colored_glyph = 0u;
out.is_cursor = 0u;
out.cursor_shape = 0u;
out.cursor_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.cursor_uv = vec2<f32>(0.0, 0.0);
out.cell_size = vec2<f32>(0.0, 0.0);
out.underline_uv = vec2<f32>(0.0, 0.0);
out.strike_uv = vec2<f32>(0.0, 0.0);
out.decoration_fg = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.has_underline = 0u;
out.has_strikethrough = 0u;
out.glyph_layer = 0;
out.cursor_layer = 0;
out.underline_layer = 0;
out.strike_layer = 0;
return out; return out;
} }
@@ -536,10 +574,29 @@ fn vs_cell_glyph(
let underline_sprite_idx = decoration_type_to_sprite(decoration_type); let underline_sprite_idx = decoration_type_to_sprite(decoration_type);
let has_decorations = underline_sprite_idx > 0u || has_strike; let has_decorations = underline_sprite_idx > 0u || has_strike;
// Skip if no glyph AND no decorations // Skip if no glyph AND no decorations - use off-screen position with valid W
if sprite_idx == 0u && !has_decorations { if sprite_idx == 0u && !has_decorations {
var out: CellVertexOutput; var out: CellVertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0); out.clip_position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
out.uv = vec2<f32>(0.0, 0.0);
out.fg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.bg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.is_background = 0u;
out.is_colored_glyph = 0u;
out.is_cursor = 0u;
out.cursor_shape = 0u;
out.cursor_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.cursor_uv = vec2<f32>(0.0, 0.0);
out.cell_size = vec2<f32>(0.0, 0.0);
out.underline_uv = vec2<f32>(0.0, 0.0);
out.strike_uv = vec2<f32>(0.0, 0.0);
out.decoration_fg = vec4<f32>(0.0, 0.0, 0.0, 0.0);
out.has_underline = 0u;
out.has_strikethrough = 0u;
out.glyph_layer = 0;
out.cursor_layer = 0;
out.underline_layer = 0;
out.strike_layer = 0;
return out; return out;
} }
+8 -1
View File
@@ -2038,7 +2038,14 @@ impl App {
self.config.edge_glow_intensity, self.config.edge_glow_intensity,
&statusline_content, &statusline_content,
) { ) {
Ok(_) => {} Ok(_) => {
// Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean)
for (pane_id, _) in &geometries {
if let Some(pane) = tab.panes.get_mut(pane_id) {
pane.terminal.clear_dirty_lines();
}
}
}
Err(wgpu::SurfaceError::Lost) => { Err(wgpu::SurfaceError::Lost) => {
renderer.resize(renderer.width, renderer.height); renderer.resize(renderer.width, renderer.height);
} }
+83 -37
View File
@@ -2148,6 +2148,10 @@ impl Renderer {
let rows = terminal.rows; let rows = terminal.rows;
let total_cells = cols * rows; let total_cells = cols * rows;
// TEMPORARY DEBUG: Force full rebuild every frame to test if dirty-line tracking is the issue
// TODO: Remove this once the rendering bug is fixed
self.cells_dirty = true;
// Check if grid size changed - need full rebuild // Check if grid size changed - need full rebuild
let size_changed = self.last_grid_size != (cols, rows); let size_changed = self.last_grid_size != (cols, rows);
if size_changed { if size_changed {
@@ -2156,10 +2160,6 @@ impl Renderer {
self.cells_dirty = true; self.cells_dirty = true;
} }
// Get dirty lines bitmap BEFORE first pass - we only need to create sprites for dirty lines
// This is a key optimization: sprites are cached, so we only need to check lines that changed
let dirty_bitmap = terminal.get_dirty_lines();
// First pass: ensure all characters have sprites // First pass: ensure all characters have sprites
// This needs mutable access to self for sprite creation // This needs mutable access to self for sprite creation
// Like Kitty's render_line(), detect PUA+space patterns for multi-cell rendering // Like Kitty's render_line(), detect PUA+space patterns for multi-cell rendering
@@ -2167,18 +2167,8 @@ impl Renderer {
// OPTIMIZATION: Use get_visible_row() to avoid Vec allocation // OPTIMIZATION: Use get_visible_row() to avoid Vec allocation
for row_idx in 0..rows { for row_idx in 0..rows {
// Skip clean lines (unless size changed, which sets cells_dirty) // Skip clean lines (unless size changed, which sets cells_dirty)
if !self.cells_dirty { if !self.cells_dirty && !terminal.is_line_dirty(row_idx) {
if row_idx < 64 { continue;
let bit = 1u64 << row_idx;
if (dirty_bitmap & bit) == 0 {
continue;
}
}
// For rows >= 64, we conservatively process them if any dirty bit is set
// (same as the second pass behavior)
else if dirty_bitmap == 0 {
continue;
}
} }
let Some(row) = terminal.get_visible_row(row_idx) else { let Some(row) = terminal.get_visible_row(row_idx) else {
@@ -2339,37 +2329,53 @@ impl Renderer {
// Second pass: convert cells to GPU format // Second pass: convert cells to GPU format
// OPTIMIZATION: Use get_visible_row() to avoid Vec allocation // OPTIMIZATION: Use get_visible_row() to avoid Vec allocation
// dirty_bitmap already fetched above before first pass
let mut any_updated = false; let mut any_updated = false;
// DEBUG: Log grid dimensions and buffer state
static DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let frame_num = DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if frame_num % 60 == 0 { // Log every 60 frames (~1 second at 60fps)
log::info!("DEBUG update_gpu_cells: cols={} rows={} total={} gpu_cells.len={} cells_dirty={}",
cols, rows, total_cells, self.gpu_cells.len(), self.cells_dirty);
}
// If we did a full reset or size changed, update all lines // If we did a full reset or size changed, update all lines
if self.cells_dirty { if self.cells_dirty {
static ROW_DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let row_frame = ROW_DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if row_frame % 60 == 0 {
let first_col: String = (0..rows).filter_map(|r| {
terminal.get_visible_row(r).and_then(|row| {
row.first().map(|cell| {
let c = cell.character;
if c == '\0' { ' ' } else { c }
})
})
}).collect();
log::info!("DEBUG col0: \"{}\"", first_col);
}
for row_idx in 0..rows { for row_idx in 0..rows {
if let Some(row) = terminal.get_visible_row(row_idx) { if let Some(row) = terminal.get_visible_row(row_idx) {
let start = row_idx * cols; let start = row_idx * cols;
let end = start + cols; let end = start + cols;
if end > self.gpu_cells.len() {
log::error!("DEBUG BUG: row_idx={} start={} end={} but gpu_cells.len={}",
row_idx, start, end, self.gpu_cells.len());
continue;
}
Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map); Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map);
} }
} }
self.cells_dirty = false; self.cells_dirty = false;
any_updated = true; any_updated = true;
} else { } else {
// Only update dirty lines // Only update dirty lines - use is_line_dirty() which handles all 256 lines
for row_idx in 0..rows.min(64) { for row_idx in 0..rows {
let bit = 1u64 << row_idx; if terminal.is_line_dirty(row_idx) {
if (dirty_bitmap & bit) != 0 {
if let Some(row) = terminal.get_visible_row(row_idx) {
let start = row_idx * cols;
let end = start + cols;
Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map);
any_updated = true;
}
}
}
// For terminals with more than 64 rows, check additional dirty_lines words
if rows > 64 && dirty_bitmap != 0 {
for row_idx in 64..rows {
if let Some(row) = terminal.get_visible_row(row_idx) { if let Some(row) = terminal.get_visible_row(row_idx) {
let start = row_idx * cols; let start = row_idx * cols;
let end = start + cols; let end = start + cols;
@@ -4601,9 +4607,8 @@ impl Renderer {
} }
} }
// Calculate pane dimensions in cells let cols = terminal.cols as u32;
let cols = (pane_width / self.cell_metrics.cell_width as f32).floor() as u32; let rows = terminal.rows as u32;
let rows = (pane_height / self.cell_metrics.cell_height as f32).floor() as u32;
// Use the actual gpu_cells size for buffer allocation (terminal.cols * terminal.rows) // Use the actual gpu_cells size for buffer allocation (terminal.cols * terminal.rows)
// This may differ from pane pixel dimensions due to rounding // This may differ from pane pixel dimensions due to rounding
@@ -4638,6 +4643,35 @@ impl Renderer {
selection_end_row: sel_end_row, selection_end_row: sel_end_row,
}; };
// DEBUG: Log grid params every 60 frames
static PANE_DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let pane_frame = PANE_DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if pane_frame % 60 == 0 {
log::info!("DEBUG pane {}: grid_params cols={} rows={} gpu_cells.len={} expected={}",
info.pane_id, grid_params.cols, grid_params.rows,
self.gpu_cells.len(), (grid_params.cols * grid_params.rows) as usize);
// Sample a few cells to see if sprite indices look reasonable
if !self.gpu_cells.is_empty() {
let sample_indices = [0, 1, 2, cols as usize, cols as usize + 1];
for &idx in &sample_indices {
if idx < self.gpu_cells.len() {
let cell = &self.gpu_cells[idx];
let sprite_idx = cell.sprite_idx & !0x80000000;
log::info!("DEBUG cell[{}]: sprite_idx={} fg={:#x} bg={:#x}",
idx, sprite_idx, cell.fg, cell.bg);
if sprite_idx > 0 && (sprite_idx as usize) < self.sprite_info.len() {
let sprite = &self.sprite_info[sprite_idx as usize];
log::info!("DEBUG sprite[{}]: uv=({:.3},{:.3},{:.3},{:.3}) layer={} size=({:.1},{:.1})",
sprite_idx, sprite.uv[0], sprite.uv[1], sprite.uv[2], sprite.uv[3],
sprite.layer, sprite.size[0], sprite.size[1]);
}
}
}
}
}
// Upload this pane's cell data to its own buffer (like Kitty's send_cell_data_to_gpu) // Upload this pane's cell data to its own buffer (like Kitty's send_cell_data_to_gpu)
// This happens BEFORE the render pass, so each pane has its own data // This happens BEFORE the render pass, so each pane has its own data
if let Some(pane_res) = self.pane_resources.get(&info.pane_id) { if let Some(pane_res) = self.pane_resources.get(&info.pane_id) {
@@ -5027,6 +5061,17 @@ impl Renderer {
let (vp_x, vp_y, vp_w, vp_h) = pane_data.viewport; let (vp_x, vp_y, vp_w, vp_h) = pane_data.viewport;
render_pass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0); render_pass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
// Set scissor rect to clip rendering to pane bounds
let scissor_x = (vp_x.round().max(0.0) as u32).min(self.width);
let scissor_y = (vp_y.round().max(0.0) as u32).min(self.height);
let scissor_w = (vp_w.round() as u32).min(self.width.saturating_sub(scissor_x));
let scissor_h = (vp_h.round() as u32).min(self.height.saturating_sub(scissor_y));
if scissor_w == 0 || scissor_h == 0 {
continue;
}
render_pass.set_scissor_rect(scissor_x, scissor_y, scissor_w, scissor_h);
// Draw cell backgrounds // Draw cell backgrounds
render_pass.set_pipeline(&self.cell_bg_pipeline); render_pass.set_pipeline(&self.cell_bg_pipeline);
render_pass.set_bind_group(0, &self.glyph_bind_group, &[]); // Atlas (shared) render_pass.set_bind_group(0, &self.glyph_bind_group, &[]); // Atlas (shared)
@@ -5041,8 +5086,9 @@ impl Renderer {
} }
} }
// Restore full-screen viewport for remaining rendering (statusline, overlays) // Restore full-screen viewport and scissor for remaining rendering (statusline, overlays)
render_pass.set_viewport(0.0, 0.0, self.width as f32, self.height as f32, 0.0, 1.0); render_pass.set_viewport(0.0, 0.0, self.width as f32, self.height as f32, 0.0, 1.0);
render_pass.set_scissor_rect(0, 0, self.width, self.height);
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// STATUSLINE RENDERING (dedicated shader) // STATUSLINE RENDERING (dedicated shader)
+51 -6
View File
@@ -252,6 +252,11 @@ impl ColorPalette {
} }
/// Saved cursor state for DECSC/DECRC. /// Saved cursor state for DECSC/DECRC.
/// Per ECMA-48 and DEC VT standards, DECSC saves:
/// - Cursor position (col, row)
/// - Character attributes (fg, bg, bold, italic, underline, strikethrough)
/// - Origin mode (DECOM) - affects cursor positioning relative to scroll region
/// - Auto-wrap mode (DECAWM) - affects line wrapping behavior
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
struct SavedCursor { struct SavedCursor {
col: usize, col: usize,
@@ -262,6 +267,8 @@ struct SavedCursor {
italic: bool, italic: bool,
underline_style: u8, underline_style: u8,
strikethrough: bool, strikethrough: bool,
origin_mode: bool,
auto_wrap: bool,
} }
/// Alternate screen buffer state. /// Alternate screen buffer state.
@@ -1374,6 +1381,26 @@ impl Handler for Terminal {
/// Handle a chunk of decoded text (Unicode codepoints as u32). /// Handle a chunk of decoded text (Unicode codepoints as u32).
/// This includes control characters (0x00-0x1F except ESC). /// This includes control characters (0x00-0x1F except ESC).
fn text(&mut self, codepoints: &[u32]) { fn text(&mut self, codepoints: &[u32]) {
// DEBUG: Detect CSI sequence content appearing as text (indicates parser bug)
// Look for patterns like "38;2;128" or "4;64;64m" - these are SGR parameters
if codepoints.len() >= 3 {
let has_semicolon = codepoints.iter().any(|&c| c == 0x3B); // ';'
let has_m = codepoints.iter().any(|&c| c == 0x6D); // 'm'
let mostly_digits = codepoints
.iter()
.filter(|&&c| c >= 0x30 && c <= 0x39)
.count()
> codepoints.len() / 2;
if has_semicolon && mostly_digits {
let text: String = codepoints
.iter()
.filter_map(|&c| char::from_u32(c))
.collect();
log::error!("DEBUG CSI LEAK: text handler received CSI-like content: {:?} at ({}, {})",
text, self.cursor_col, self.cursor_row);
}
}
#[cfg(feature = "render_timing")] #[cfg(feature = "render_timing")]
let start = std::time::Instant::now(); let start = std::time::Instant::now();
@@ -1866,6 +1893,11 @@ impl Handler for Terminal {
match mode { match mode {
0 => self.clear_line_from_cursor(), 0 => self.clear_line_from_cursor(),
1 => { 1 => {
log::warn!(
"DEBUG EL1 (erase to cursor): row={} cursor_col={}",
self.cursor_row,
self.cursor_col
);
let grid_row = self.line_map[self.cursor_row]; let grid_row = self.line_map[self.cursor_row];
for col in 0..=self.cursor_col { for col in 0..=self.cursor_col {
self.grid[grid_row][col] = blank; self.grid[grid_row][col] = blank;
@@ -1873,6 +1905,10 @@ impl Handler for Terminal {
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
2 => { 2 => {
log::warn!(
"DEBUG EL2 (erase whole line): row={}",
self.cursor_row
);
let grid_row = self.line_map[self.cursor_row]; let grid_row = self.line_map[self.cursor_row];
self.grid[grid_row].fill(blank); self.grid[grid_row].fill(blank);
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
@@ -2028,8 +2064,9 @@ impl Handler for Terminal {
&mut self.scroll_bottom, &mut self.scroll_bottom,
); );
} }
// Move cursor to home position // Move cursor to home position (respects origin mode)
self.cursor_row = 0; self.cursor_row =
if self.origin_mode { self.scroll_top } else { 0 };
self.cursor_col = 0; self.cursor_col = 0;
} }
// Window manipulation (CSI Ps t) - XTWINOPS // Window manipulation (CSI Ps t) - XTWINOPS
@@ -2130,11 +2167,15 @@ impl Handler for Terminal {
italic: self.current_italic, italic: self.current_italic,
underline_style: self.current_underline_style, underline_style: self.current_underline_style,
strikethrough: self.current_strikethrough, strikethrough: self.current_strikethrough,
origin_mode: self.origin_mode,
auto_wrap: self.auto_wrap,
}; };
log::debug!( log::debug!(
"ESC 7: Cursor saved at ({}, {})", "ESC 7: Cursor saved at ({}, {}), origin_mode={}, auto_wrap={}",
self.cursor_col, self.cursor_col,
self.cursor_row self.cursor_row,
self.origin_mode,
self.auto_wrap
); );
} }
@@ -2149,10 +2190,14 @@ impl Handler for Terminal {
self.current_italic = self.saved_cursor.italic; self.current_italic = self.saved_cursor.italic;
self.current_underline_style = self.saved_cursor.underline_style; self.current_underline_style = self.saved_cursor.underline_style;
self.current_strikethrough = self.saved_cursor.strikethrough; self.current_strikethrough = self.saved_cursor.strikethrough;
self.origin_mode = self.saved_cursor.origin_mode;
self.auto_wrap = self.saved_cursor.auto_wrap;
log::debug!( log::debug!(
"ESC 8: Cursor restored to ({}, {})", "ESC 8: Cursor restored to ({}, {}), origin_mode={}, auto_wrap={}",
self.cursor_col, self.cursor_col,
self.cursor_row self.cursor_row,
self.origin_mode,
self.auto_wrap
); );
} }
+407 -159
View File
@@ -12,8 +12,8 @@
//! 5. Buffer is integrated into parser - I/O writes directly here //! 5. Buffer is integrated into parser - I/O writes directly here
//! 6. Lock is released during parsing - I/O can continue while main parses //! 6. Lock is released during parsing - I/O can continue while main parses
use std::sync::Mutex;
use crate::simd_utf8::SimdUtf8Decoder; use crate::simd_utf8::SimdUtf8Decoder;
use std::sync::Mutex;
/// Buffer size - 1MB like Kitty /// Buffer size - 1MB like Kitty
pub const BUF_SIZE: usize = 1024 * 1024; pub const BUF_SIZE: usize = 1024 * 1024;
@@ -166,7 +166,8 @@ impl CsiParams {
fn add_digit(&mut self, digit: u8) { fn add_digit(&mut self, digit: u8) {
// Like Kitty: accumulate with multipliers, divide at commit // Like Kitty: accumulate with multipliers, divide at commit
if self.num_digits < DIGIT_MULTIPLIERS.len() { if self.num_digits < DIGIT_MULTIPLIERS.len() {
self.accumulator += (digit - b'0') as i64 * DIGIT_MULTIPLIERS[self.num_digits]; self.accumulator +=
(digit - b'0') as i64 * DIGIT_MULTIPLIERS[self.num_digits];
self.num_digits += 1; self.num_digits += 1;
} }
} }
@@ -183,7 +184,8 @@ impl CsiParams {
0 0
} else { } else {
// Division converts from reverse-order accumulation // Division converts from reverse-order accumulation
(self.accumulator / DIGIT_MULTIPLIERS[self.num_digits - 1]) as i32 * self.multiplier (self.accumulator / DIGIT_MULTIPLIERS[self.num_digits - 1]) as i32
* self.multiplier
}; };
self.params[self.num_params] = value; self.params[self.num_params] = value;
self.num_params += 1; self.num_params += 1;
@@ -288,9 +290,13 @@ unsafe impl Send for SharedParser {}
impl SharedParser { impl SharedParser {
/// Create a new shared parser with integrated buffer. /// Create a new shared parser with integrated buffer.
pub fn new() -> Self { pub fn new() -> Self {
let wakeup_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) }; let wakeup_fd =
unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
if wakeup_fd < 0 { if wakeup_fd < 0 {
panic!("Failed to create eventfd: {}", std::io::Error::last_os_error()); panic!(
"Failed to create eventfd: {}",
std::io::Error::last_os_error()
);
} }
Self { Self {
@@ -309,7 +315,9 @@ impl SharedParser {
vte_state: std::cell::UnsafeCell::new(State::Normal), vte_state: std::cell::UnsafeCell::new(State::Normal),
csi: std::cell::UnsafeCell::new(CsiParams::default()), csi: std::cell::UnsafeCell::new(CsiParams::default()),
utf8: std::cell::UnsafeCell::new(SimdUtf8Decoder::new()), utf8: std::cell::UnsafeCell::new(SimdUtf8Decoder::new()),
codepoint_buf: std::cell::UnsafeCell::new(Vec::with_capacity(BUF_SIZE)), codepoint_buf: std::cell::UnsafeCell::new(Vec::with_capacity(
BUF_SIZE,
)),
osc_buffer: std::cell::UnsafeCell::new(Vec::new()), osc_buffer: std::cell::UnsafeCell::new(Vec::new()),
string_buffer: std::cell::UnsafeCell::new(Vec::new()), string_buffer: std::cell::UnsafeCell::new(Vec::new()),
escape_len: std::cell::UnsafeCell::new(0), escape_len: std::cell::UnsafeCell::new(0),
@@ -358,7 +366,8 @@ impl SharedParser {
return 0; return 0;
} }
let result = unsafe { libc::read(fd, ptr as *mut libc::c_void, available) }; let result =
unsafe { libc::read(fd, ptr as *mut libc::c_void, available) };
if result > 0 { if result > 0 {
self.commit_write(result as usize); self.commit_write(result as usize);
@@ -370,7 +379,11 @@ impl SharedParser {
pub fn drain_wakeup(&self) { pub fn drain_wakeup(&self) {
let mut buf = 0u64; let mut buf = 0u64;
unsafe { unsafe {
libc::read(self.wakeup_fd, &mut buf as *mut u64 as *mut libc::c_void, 8); libc::read(
self.wakeup_fd,
&mut buf as *mut u64 as *mut libc::c_void,
8,
);
} }
} }
@@ -409,6 +422,15 @@ impl SharedParser {
// Reset consumed counter for this parse pass (like Kitty: self->read.consumed = 0) // Reset consumed counter for this parse pass (like Kitty: self->read.consumed = 0)
state.read_consumed = 0; state.read_consumed = 0;
// Check vte_state at start of parse pass
let vte_state_at_start = unsafe { *self.vte_state.get() };
if vte_state_at_start != State::Normal {
log::error!(
"DEBUG run_parse_pass START: vte_state={:?} read_pos={} read_sz={}",
vte_state_at_start, state.read_pos, state.read_sz
);
}
// Copy positions to UnsafeCell fields for use while lock is released // Copy positions to UnsafeCell fields for use while lock is released
unsafe { unsafe {
*self.parse_pos.get() = state.read_pos; *self.parse_pos.get() = state.read_pos;
@@ -473,6 +495,29 @@ impl SharedParser {
if state.read_consumed > 0 { if state.read_consumed > 0 {
let old_sz = state.read_sz; let old_sz = state.read_sz;
// Debug: Check what we're about to discard and what state we're in
let vte_state = unsafe { *self.vte_state.get() };
if vte_state != State::Normal {
let buf = unsafe { &*self.buf.get() };
let remaining_start = state.read_consumed.min(old_sz);
let preview_len = (old_sz - remaining_start).min(20);
let preview: String = buf
[remaining_start..remaining_start + preview_len]
.iter()
.map(|&b| {
if b >= 0x20 && b < 0x7f {
b as char
} else {
'.'
}
})
.collect();
log::error!(
"DEBUG COMPACT: state={:?} consumed={} old_sz={} remaining preview: {:?}",
vte_state, state.read_consumed, old_sz, preview
);
}
// Like Kitty: pos -= consumed, sz -= consumed, memmove // Like Kitty: pos -= consumed, sz -= consumed, memmove
state.read_pos = state.read_pos.saturating_sub(state.read_consumed); state.read_pos = state.read_pos.saturating_sub(state.read_consumed);
state.read_sz = state.read_sz.saturating_sub(state.read_consumed); state.read_sz = state.read_sz.saturating_sub(state.read_consumed);
@@ -500,7 +545,11 @@ impl SharedParser {
drop(state); drop(state);
let val = 1u64; let val = 1u64;
unsafe { unsafe {
libc::write(self.wakeup_fd, &val as *const u64 as *const libc::c_void, 8); libc::write(
self.wakeup_fd,
&val as *const u64 as *const libc::c_void,
8,
);
} }
return parsed_any; return parsed_any;
} }
@@ -545,27 +594,68 @@ impl SharedParser {
let escape_len = unsafe { &mut *self.escape_len.get() }; let escape_len = unsafe { &mut *self.escape_len.get() };
let buf = unsafe { &*self.buf.get() }; let buf = unsafe { &*self.buf.get() };
// Debug: Log state at start of consume_input
if *vte_state != State::Normal {
log::error!(
"DEBUG consume_input START: state={:?} pos={} consumed={} sz={}",
*vte_state, *parse_pos, *parse_consumed, parse_sz
);
}
// Debug: Check if we're starting in Normal with CSI-like content
if *vte_state == State::Normal
&& *parse_pos < parse_sz
&& buf[*parse_pos] == b'['
{
log::error!(
"DEBUG: Starting consume_input in Normal but buf[{}]='[' (0x5b). consumed={} sz={}",
*parse_pos, *parse_consumed, parse_sz
);
}
// Loop until buffer exhausted or waiting for more data // Loop until buffer exhausted or waiting for more data
while *parse_pos < parse_sz { while *parse_pos < parse_sz {
match *vte_state { match *vte_state {
State::Normal => { State::Normal => {
// Like Kitty: consume_normal(self); self->read.consumed = self->read.pos; Self::consume_normal_impl(
Self::consume_normal_impl(handler, buf, parse_pos, parse_sz, utf8, codepoint_buf, vte_state, escape_len); handler,
buf,
parse_pos,
parse_sz,
utf8,
codepoint_buf,
vte_state,
escape_len,
);
*parse_consumed = *parse_pos; *parse_consumed = *parse_pos;
// consume_normal_impl sets vte_state to Escape if ESC found, so loop continues
} }
State::Escape => { State::Escape => {
// Like Kitty: if (consume_esc(self)) { self->read.consumed = self->read.pos; } let state_before = *vte_state;
if Self::consume_escape_impl(handler, buf, parse_pos, parse_sz, *parse_consumed, vte_state, csi, osc_buffer, string_buffer, escape_len) { if Self::consume_escape_impl(
handler,
buf,
parse_pos,
parse_sz,
*parse_consumed,
vte_state,
csi,
osc_buffer,
string_buffer,
escape_len,
) {
*parse_consumed = *parse_pos; *parse_consumed = *parse_pos;
// State changed, continue loop } else if *vte_state != state_before {
// State changed to multi-byte sequence (CSI, OSC, etc.)
// Continue loop WITHOUT updating parse_consumed
} else { } else {
// Need more data for escape sequence // Need more data for escape sequence
break; break;
} }
} }
State::EscapeIntermediate(_) => { State::EscapeIntermediate(_) => {
if Self::consume_escape_intermediate_impl(handler, buf, parse_pos, parse_sz, vte_state) { if Self::consume_escape_intermediate_impl(
handler, buf, parse_pos, parse_sz, vte_state,
) {
*parse_consumed = *parse_pos; *parse_consumed = *parse_pos;
} else { } else {
break; break;
@@ -573,7 +663,15 @@ impl SharedParser {
} }
State::Csi => { State::Csi => {
// Like Kitty: if (consume_csi(self)) { self->read.consumed = self->read.pos; dispatch; SET_STATE(NORMAL); } // Like Kitty: if (consume_csi(self)) { self->read.consumed = self->read.pos; dispatch; SET_STATE(NORMAL); }
if Self::consume_csi_impl(handler, buf, parse_pos, parse_sz, *parse_consumed, csi, escape_len) { if Self::consume_csi_impl(
handler,
buf,
parse_pos,
parse_sz,
*parse_consumed,
csi,
escape_len,
) {
*parse_consumed = *parse_pos; *parse_consumed = *parse_pos;
if csi.is_valid { if csi.is_valid {
handler.csi(csi); handler.csi(csi);
@@ -586,7 +684,10 @@ impl SharedParser {
} }
} }
State::Osc => { State::Osc => {
if Self::consume_osc_impl(handler, buf, parse_pos, parse_sz, vte_state, osc_buffer, escape_len) { if Self::consume_osc_impl(
handler, buf, parse_pos, parse_sz, vte_state,
osc_buffer, escape_len,
) {
*parse_consumed = *parse_pos; *parse_consumed = *parse_pos;
*vte_state = State::Normal; *vte_state = State::Normal;
} else { } else {
@@ -594,7 +695,15 @@ impl SharedParser {
} }
} }
State::Dcs | State::Apc | State::Pm | State::Sos => { State::Dcs | State::Apc | State::Pm | State::Sos => {
if Self::consume_string_impl(handler, buf, parse_pos, parse_sz, vte_state, string_buffer, escape_len) { if Self::consume_string_impl(
handler,
buf,
parse_pos,
parse_sz,
vte_state,
string_buffer,
escape_len,
) {
*parse_consumed = *parse_pos; *parse_consumed = *parse_pos;
*vte_state = State::Normal; *vte_state = State::Normal;
} else { } else {
@@ -627,7 +736,8 @@ impl SharedParser {
} }
let remaining = &buf[*parse_pos..parse_sz]; let remaining = &buf[*parse_pos..parse_sz];
let (consumed, found_esc) = utf8.decode_to_esc(remaining, codepoint_buf); let (consumed, found_esc) =
utf8.decode_to_esc(remaining, codepoint_buf);
*parse_pos += consumed; *parse_pos += consumed;
if !codepoint_buf.is_empty() { if !codepoint_buf.is_empty() {
@@ -670,28 +780,83 @@ impl SharedParser {
if is_first_char { if is_first_char {
match ch { match ch {
b'[' => { *vte_state = State::Csi; csi.reset(); } // Multi-byte sequences: return false so parse_consumed is NOT updated.
b']' => { *vte_state = State::Osc; osc_buffer.clear(); } // This prevents ESC[ from being discarded on buffer compaction before
b'P' => { *vte_state = State::Dcs; string_buffer.clear(); } // the full sequence completes.
b'_' => { *vte_state = State::Apc; string_buffer.clear(); } b'[' => {
b'^' => { *vte_state = State::Pm; string_buffer.clear(); } *vte_state = State::Csi;
b'X' => { *vte_state = State::Sos; string_buffer.clear(); } csi.reset();
// Two-char sequences - need another char return false;
b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/' | b'%' | b'#' | b' ' => { }
*vte_state = State::EscapeIntermediate(ch); b']' => {
return false; // Need more chars *vte_state = State::Osc;
osc_buffer.clear();
return false;
}
b'P' => {
*vte_state = State::Dcs;
string_buffer.clear();
return false;
}
b'_' => {
*vte_state = State::Apc;
string_buffer.clear();
return false;
}
b'^' => {
*vte_state = State::Pm;
string_buffer.clear();
return false;
}
b'X' => {
*vte_state = State::Sos;
string_buffer.clear();
return false;
}
b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/' | b'%'
| b'#' | b' ' => {
*vte_state = State::EscapeIntermediate(ch);
return false;
}
b'7' => {
handler.save_cursor();
*vte_state = State::Normal;
}
b'8' => {
handler.restore_cursor();
*vte_state = State::Normal;
}
b'c' => {
handler.reset();
*vte_state = State::Normal;
}
b'D' => {
handler.index();
*vte_state = State::Normal;
}
b'E' => {
handler.newline();
*vte_state = State::Normal;
}
b'H' => {
handler.set_tab_stop();
*vte_state = State::Normal;
}
b'M' => {
handler.reverse_index();
*vte_state = State::Normal;
}
b'=' => {
handler.set_keypad_mode(true);
*vte_state = State::Normal;
}
b'>' => {
handler.set_keypad_mode(false);
*vte_state = State::Normal;
}
b'\\' => {
*vte_state = State::Normal;
} }
// Single-char escape sequences
b'7' => { handler.save_cursor(); *vte_state = State::Normal; }
b'8' => { handler.restore_cursor(); *vte_state = State::Normal; }
b'c' => { handler.reset(); *vte_state = State::Normal; }
b'D' => { handler.index(); *vte_state = State::Normal; }
b'E' => { handler.newline(); *vte_state = State::Normal; }
b'H' => { handler.set_tab_stop(); *vte_state = State::Normal; }
b'M' => { handler.reverse_index(); *vte_state = State::Normal; }
b'=' => { handler.set_keypad_mode(true); *vte_state = State::Normal; }
b'>' => { handler.set_keypad_mode(false); *vte_state = State::Normal; }
b'\\' => { *vte_state = State::Normal; } // ST
_ => { _ => {
log::debug!("Unknown escape sequence: ESC {:02x}", ch); log::debug!("Unknown escape sequence: ESC {:02x}", ch);
*vte_state = State::Normal; *vte_state = State::Normal;
@@ -736,7 +901,10 @@ impl SharedParser {
let intermediate = match *vte_state { let intermediate = match *vte_state {
State::EscapeIntermediate(i) => i, State::EscapeIntermediate(i) => i,
_ => { *vte_state = State::Normal; return true; } _ => {
*vte_state = State::Normal;
return true;
}
}; };
*vte_state = State::Normal; *vte_state = State::Normal;
@@ -781,99 +949,96 @@ impl SharedParser {
} }
match csi.state { match csi.state {
CsiState::Start => { CsiState::Start => match ch {
match ch { b';' => {
b';' => { csi.params[csi.num_params] = 0;
csi.params[csi.num_params] = 0; csi.num_params += 1;
csi.num_params += 1; csi.state = CsiState::Body;
csi.state = CsiState::Body;
}
b'0'..=b'9' => {
csi.add_digit(ch);
csi.state = CsiState::Body;
}
b'?' | b'>' | b'<' | b'=' => {
csi.primary = ch;
csi.state = CsiState::Body;
}
b'-' => {
csi.multiplier = -1;
csi.num_digits = 1;
csi.state = CsiState::Body;
}
b' ' | b'\'' | b'"' | b'!' | b'$' | b'#' | b'*' => {
csi.secondary = ch;
csi.state = CsiState::PostSecondary;
}
b'@'..=b'~' => {
csi.final_char = ch;
csi.is_valid = true;
return true;
}
_ => {
log::debug!("Invalid CSI character: {:02x}", ch);
return true;
}
} }
} b'0'..=b'9' => {
CsiState::Body => { csi.add_digit(ch);
match ch { csi.state = CsiState::Body;
b'0'..=b'9' => { }
csi.add_digit(ch); b'?' | b'>' | b'<' | b'=' => {
} csi.primary = ch;
b';' => { csi.state = CsiState::Body;
if csi.num_digits == 0 { }
csi.num_digits = 1; b'-' => {
} csi.multiplier = -1;
if !csi.commit_param() { csi.num_digits = 1;
return true; csi.state = CsiState::Body;
} }
csi.is_sub_param[csi.num_params] = false; b' ' | b'\'' | b'"' | b'!' | b'$' | b'#' | b'*' => {
} csi.secondary = ch;
b':' => { csi.state = CsiState::PostSecondary;
if !csi.commit_param() { }
return true; b'@'..=b'~' => {
} csi.final_char = ch;
csi.is_sub_param[csi.num_params] = true; csi.is_valid = true;
} return true;
b'-' if csi.num_digits == 0 => { }
csi.multiplier = -1; _ => {
log::debug!("Invalid CSI character: {:02x}", ch);
return true;
}
},
CsiState::Body => match ch {
b'0'..=b'9' => {
csi.add_digit(ch);
}
b';' => {
if csi.num_digits == 0 {
csi.num_digits = 1; csi.num_digits = 1;
} }
b' ' | b'\'' | b'"' | b'!' | b'$' | b'#' | b'*' => { if !csi.commit_param() {
if !csi.commit_param() {
return true;
}
csi.secondary = ch;
csi.state = CsiState::PostSecondary;
}
b'@'..=b'~' => {
if csi.num_digits > 0 || csi.num_params > 0 {
csi.commit_param();
}
csi.final_char = ch;
csi.is_valid = true;
return true;
}
_ => {
log::debug!("Invalid CSI body character: {:02x}", ch);
return true; return true;
} }
csi.is_sub_param[csi.num_params] = false;
} }
} b':' => {
CsiState::PostSecondary => { if !csi.commit_param() {
match ch {
b'@'..=b'~' => {
csi.final_char = ch;
csi.is_valid = true;
return true;
}
_ => {
log::debug!("Invalid CSI post-secondary character: {:02x}", ch);
return true; return true;
} }
csi.is_sub_param[csi.num_params] = true;
} }
} b'-' if csi.num_digits == 0 => {
csi.multiplier = -1;
csi.num_digits = 1;
}
b' ' | b'\'' | b'"' | b'!' | b'$' | b'#' | b'*' => {
if !csi.commit_param() {
return true;
}
csi.secondary = ch;
csi.state = CsiState::PostSecondary;
}
b'@'..=b'~' => {
if csi.num_digits > 0 || csi.num_params > 0 {
csi.commit_param();
}
csi.final_char = ch;
csi.is_valid = true;
return true;
}
_ => {
log::debug!("Invalid CSI body character: {:02x}", ch);
return true;
}
},
CsiState::PostSecondary => match ch {
b'@'..=b'~' => {
csi.final_char = ch;
csi.is_valid = true;
return true;
}
_ => {
log::debug!(
"Invalid CSI post-secondary character: {:02x}",
ch
);
return true;
}
},
} }
} }
@@ -914,7 +1079,8 @@ impl SharedParser {
} }
0x1B => { 0x1B => {
// Check for ESC \ // Check for ESC \
if *parse_pos + 1 < parse_sz && buf[*parse_pos + 1] == b'\\' { if *parse_pos + 1 < parse_sz && buf[*parse_pos + 1] == b'\\'
{
*parse_pos += 2; *parse_pos += 2;
handler.osc(osc_buffer); handler.osc(osc_buffer);
return true; return true;
@@ -963,14 +1129,23 @@ impl SharedParser {
0x9C => { 0x9C => {
// C1 ST terminator // C1 ST terminator
*parse_pos += 1; *parse_pos += 1;
Self::dispatch_string_command(handler, vte_state, string_buffer); Self::dispatch_string_command(
handler,
vte_state,
string_buffer,
);
return true; return true;
} }
0x1B => { 0x1B => {
// Check for ESC \ // Check for ESC \
if *parse_pos + 1 < parse_sz && buf[*parse_pos + 1] == b'\\' { if *parse_pos + 1 < parse_sz && buf[*parse_pos + 1] == b'\\'
{
*parse_pos += 2; *parse_pos += 2;
Self::dispatch_string_command(handler, vte_state, string_buffer); Self::dispatch_string_command(
handler,
vte_state,
string_buffer,
);
return true; return true;
} else if *parse_pos + 1 < parse_sz { } else if *parse_pos + 1 < parse_sz {
// ESC not followed by \ - include in buffer // ESC not followed by \ - include in buffer
@@ -1047,14 +1222,20 @@ impl Parser {
/// Process a buffer of bytes, calling the handler for each action. /// Process a buffer of bytes, calling the handler for each action.
/// Returns the number of bytes consumed. /// Returns the number of bytes consumed.
pub fn parse<H: Handler>(&mut self, bytes: &[u8], handler: &mut H) -> usize { pub fn parse<H: Handler>(
&mut self,
bytes: &[u8],
handler: &mut H,
) -> usize {
let mut pos = 0; let mut pos = 0;
while pos < bytes.len() { while pos < bytes.len() {
match self.state { match self.state {
State::Normal => { State::Normal => {
// Fast path: UTF-8 decode until ESC using SIMD // Fast path: UTF-8 decode until ESC using SIMD
let (consumed, found_esc) = self.utf8.decode_to_esc(&bytes[pos..], &mut self.codepoint_buf); let (consumed, found_esc) = self
.utf8
.decode_to_esc(&bytes[pos..], &mut self.codepoint_buf);
// Process decoded codepoints (text + control chars) // Process decoded codepoints (text + control chars)
if !self.codepoint_buf.is_empty() { if !self.codepoint_buf.is_empty() {
@@ -1072,7 +1253,8 @@ impl Parser {
pos += self.consume_escape(bytes, pos, handler); pos += self.consume_escape(bytes, pos, handler);
} }
State::EscapeIntermediate(_) => { State::EscapeIntermediate(_) => {
pos += self.consume_escape_intermediate(bytes, pos, handler); pos +=
self.consume_escape_intermediate(bytes, pos, handler);
} }
State::Csi => { State::Csi => {
pos += self.consume_csi(bytes, pos, handler); pos += self.consume_csi(bytes, pos, handler);
@@ -1090,7 +1272,12 @@ impl Parser {
} }
/// Process bytes after ESC. /// Process bytes after ESC.
fn consume_escape<H: Handler>(&mut self, bytes: &[u8], pos: usize, handler: &mut H) -> usize { fn consume_escape<H: Handler>(
&mut self,
bytes: &[u8],
pos: usize,
handler: &mut H,
) -> usize {
if pos >= bytes.len() { if pos >= bytes.len() {
return 0; return 0;
} }
@@ -1136,7 +1323,8 @@ impl Parser {
1 1
} }
// Two-char sequences: ESC ( ESC ) ESC # ESC % ESC SP etc. // Two-char sequences: ESC ( ESC ) ESC # ESC % ESC SP etc.
b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/' | b'%' | b'#' | b' ' => { b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/' | b'%' | b'#'
| b' ' => {
self.state = State::EscapeIntermediate(ch); self.state = State::EscapeIntermediate(ch);
1 1
} }
@@ -1210,7 +1398,12 @@ impl Parser {
} }
/// Process second byte of two-char escape sequence. /// Process second byte of two-char escape sequence.
fn consume_escape_intermediate<H: Handler>(&mut self, bytes: &[u8], pos: usize, handler: &mut H) -> usize { fn consume_escape_intermediate<H: Handler>(
&mut self,
bytes: &[u8],
pos: usize,
handler: &mut H,
) -> usize {
if pos >= bytes.len() { if pos >= bytes.len() {
return 0; return 0;
} }
@@ -1249,7 +1442,12 @@ impl Parser {
} }
/// Process CSI sequence bytes. /// Process CSI sequence bytes.
fn consume_csi<H: Handler>(&mut self, bytes: &[u8], pos: usize, handler: &mut H) -> usize { fn consume_csi<H: Handler>(
&mut self,
bytes: &[u8],
pos: usize,
handler: &mut H,
) -> usize {
let mut consumed = 0; let mut consumed = 0;
while pos + consumed < bytes.len() { while pos + consumed < bytes.len() {
@@ -1347,7 +1545,9 @@ impl Parser {
} }
// Final byte // Final byte
b'@'..=b'~' => { b'@'..=b'~' => {
if self.csi.num_digits > 0 || self.csi.num_params > 0 { if self.csi.num_digits > 0
|| self.csi.num_params > 0
{
self.csi.commit_param(); self.csi.commit_param();
} }
self.csi.final_char = ch; self.csi.final_char = ch;
@@ -1357,7 +1557,10 @@ impl Parser {
return consumed; return consumed;
} }
_ => { _ => {
log::debug!("Invalid CSI body character: {:02x}", ch); log::debug!(
"Invalid CSI body character: {:02x}",
ch
);
self.state = State::Normal; self.state = State::Normal;
return consumed; return consumed;
} }
@@ -1374,7 +1577,10 @@ impl Parser {
return consumed; return consumed;
} }
_ => { _ => {
log::debug!("Invalid CSI post-secondary character: {:02x}", ch); log::debug!(
"Invalid CSI post-secondary character: {:02x}",
ch
);
self.state = State::Normal; self.state = State::Normal;
return consumed; return consumed;
} }
@@ -1393,7 +1599,12 @@ impl Parser {
/// Process OSC sequence bytes using SIMD-accelerated terminator search. /// Process OSC sequence bytes using SIMD-accelerated terminator search.
/// Like Kitty's find_st_terminator + accumulate_st_terminated_esc_code. /// Like Kitty's find_st_terminator + accumulate_st_terminated_esc_code.
fn consume_osc<H: Handler>(&mut self, bytes: &[u8], pos: usize, handler: &mut H) -> usize { fn consume_osc<H: Handler>(
&mut self,
bytes: &[u8],
pos: usize,
handler: &mut H,
) -> usize {
let remaining = &bytes[pos..]; let remaining = &bytes[pos..];
// Use SIMD-accelerated search to find BEL (0x07), ESC (0x1B), or C1 ST (0x9C) // Use SIMD-accelerated search to find BEL (0x07), ESC (0x1B), or C1 ST (0x9C)
@@ -1403,7 +1614,9 @@ impl Parser {
let terminator = remaining[term_pos]; let terminator = remaining[term_pos];
// Check max length before accepting // Check max length before accepting
if self.escape_len + term_pos > MAX_ESCAPE_LEN || self.osc_buffer.len() + term_pos > MAX_OSC_LEN { if self.escape_len + term_pos > MAX_ESCAPE_LEN
|| self.osc_buffer.len() + term_pos > MAX_OSC_LEN
{
log::debug!("OSC sequence too long, aborting"); log::debug!("OSC sequence too long, aborting");
self.state = State::Normal; self.state = State::Normal;
return remaining.len(); return remaining.len();
@@ -1428,9 +1641,12 @@ impl Parser {
} }
0x1B => { 0x1B => {
// ESC found - check if followed by \ for ST // ESC found - check if followed by \ for ST
if term_pos + 1 < remaining.len() && remaining[term_pos + 1] == b'\\' { if term_pos + 1 < remaining.len()
&& remaining[term_pos + 1] == b'\\'
{
// ESC \ (ST) terminator // ESC \ (ST) terminator
self.osc_buffer.extend_from_slice(&remaining[..term_pos]); self.osc_buffer
.extend_from_slice(&remaining[..term_pos]);
handler.osc(&self.osc_buffer); handler.osc(&self.osc_buffer);
self.state = State::Normal; self.state = State::Normal;
self.escape_len += term_pos + 2; self.escape_len += term_pos + 2;
@@ -1438,7 +1654,8 @@ impl Parser {
} else if term_pos + 1 < remaining.len() { } else if term_pos + 1 < remaining.len() {
// ESC not followed by \ - this is a new escape sequence // ESC not followed by \ - this is a new escape sequence
// Copy everything before ESC and transition to Escape state // Copy everything before ESC and transition to Escape state
self.osc_buffer.extend_from_slice(&remaining[..term_pos]); self.osc_buffer
.extend_from_slice(&remaining[..term_pos]);
handler.osc(&self.osc_buffer); handler.osc(&self.osc_buffer);
self.state = State::Escape; self.state = State::Escape;
self.escape_len += term_pos + 1; self.escape_len += term_pos + 1;
@@ -1446,7 +1663,8 @@ impl Parser {
} else { } else {
// ESC at end of buffer, need more data // ESC at end of buffer, need more data
// Copy everything before ESC, keep ESC for next parse // Copy everything before ESC, keep ESC for next parse
self.osc_buffer.extend_from_slice(&remaining[..term_pos]); self.osc_buffer
.extend_from_slice(&remaining[..term_pos]);
self.escape_len += term_pos; self.escape_len += term_pos;
return term_pos; return term_pos;
} }
@@ -1455,7 +1673,9 @@ impl Parser {
} }
} else { } else {
// No terminator found - check max length // No terminator found - check max length
if self.escape_len + remaining.len() > MAX_ESCAPE_LEN || self.osc_buffer.len() + remaining.len() > MAX_OSC_LEN { if self.escape_len + remaining.len() > MAX_ESCAPE_LEN
|| self.osc_buffer.len() + remaining.len() > MAX_OSC_LEN
{
log::debug!("OSC sequence too long, aborting"); log::debug!("OSC sequence too long, aborting");
self.state = State::Normal; self.state = State::Normal;
return remaining.len(); return remaining.len();
@@ -1476,14 +1696,21 @@ impl Parser {
State::Apc => handler.apc(&self.string_buffer), State::Apc => handler.apc(&self.string_buffer),
State::Pm => handler.pm(&self.string_buffer), State::Pm => handler.pm(&self.string_buffer),
State::Sos => handler.sos(&self.string_buffer), State::Sos => handler.sos(&self.string_buffer),
_ => unreachable!("dispatch_string_command called in invalid state"), _ => {
unreachable!("dispatch_string_command called in invalid state")
}
} }
} }
/// Process DCS/APC/PM/SOS sequence bytes using SIMD-accelerated terminator search. /// Process DCS/APC/PM/SOS sequence bytes using SIMD-accelerated terminator search.
/// Like Kitty's find_st_terminator + accumulate_st_terminated_esc_code. /// Like Kitty's find_st_terminator + accumulate_st_terminated_esc_code.
/// Uses iterative approach to avoid stack overflow on malformed input. /// Uses iterative approach to avoid stack overflow on malformed input.
fn consume_string_command<H: Handler>(&mut self, bytes: &[u8], pos: usize, handler: &mut H) -> usize { fn consume_string_command<H: Handler>(
&mut self,
bytes: &[u8],
pos: usize,
handler: &mut H,
) -> usize {
let mut current_pos = pos; let mut current_pos = pos;
let mut total_consumed = 0; let mut total_consumed = 0;
@@ -1504,7 +1731,8 @@ impl Parser {
match terminator { match terminator {
0x9C => { 0x9C => {
// C1 ST terminator - copy data in bulk and dispatch // C1 ST terminator - copy data in bulk and dispatch
self.string_buffer.extend_from_slice(&remaining[..term_pos]); self.string_buffer
.extend_from_slice(&remaining[..term_pos]);
self.dispatch_string_command(handler); self.dispatch_string_command(handler);
self.state = State::Normal; self.state = State::Normal;
self.escape_len += term_pos + 1; self.escape_len += term_pos + 1;
@@ -1512,9 +1740,12 @@ impl Parser {
} }
0x1B => { 0x1B => {
// ESC found - check if followed by \ for ST // ESC found - check if followed by \ for ST
if term_pos + 1 < remaining.len() && remaining[term_pos + 1] == b'\\' { if term_pos + 1 < remaining.len()
&& remaining[term_pos + 1] == b'\\'
{
// ESC \ (ST) terminator // ESC \ (ST) terminator
self.string_buffer.extend_from_slice(&remaining[..term_pos]); self.string_buffer
.extend_from_slice(&remaining[..term_pos]);
self.dispatch_string_command(handler); self.dispatch_string_command(handler);
self.state = State::Normal; self.state = State::Normal;
self.escape_len += term_pos + 2; self.escape_len += term_pos + 2;
@@ -1522,7 +1753,8 @@ impl Parser {
} else if term_pos + 1 < remaining.len() { } else if term_pos + 1 < remaining.len() {
// ESC not followed by \ - include ESC in data and continue // ESC not followed by \ - include ESC in data and continue
// (Unlike OSC, string commands include raw ESC that isn't ST) // (Unlike OSC, string commands include raw ESC that isn't ST)
self.string_buffer.extend_from_slice(&remaining[..=term_pos]); self.string_buffer
.extend_from_slice(&remaining[..=term_pos]);
self.escape_len += term_pos + 1; self.escape_len += term_pos + 1;
// Continue searching from after this ESC (iterative, not recursive) // Continue searching from after this ESC (iterative, not recursive)
let consumed = term_pos + 1; let consumed = term_pos + 1;
@@ -1532,7 +1764,8 @@ impl Parser {
} else { } else {
// ESC at end of buffer, need more data // ESC at end of buffer, need more data
// Copy everything before ESC, keep ESC for next parse // Copy everything before ESC, keep ESC for next parse
self.string_buffer.extend_from_slice(&remaining[..term_pos]); self.string_buffer
.extend_from_slice(&remaining[..term_pos]);
self.escape_len += term_pos; self.escape_len += term_pos;
return total_consumed + term_pos; return total_consumed + term_pos;
} }
@@ -1684,7 +1917,10 @@ mod tests {
parser.parse(b"Hello, World!", &mut handler); parser.parse(b"Hello, World!", &mut handler);
assert_eq!(handler.text_chunks.len(), 1); assert_eq!(handler.text_chunks.len(), 1);
let text: String = handler.text_chunks[0].iter().filter_map(|&cp| char::from_u32(cp)).collect(); let text: String = handler.text_chunks[0]
.iter()
.filter_map(|&cp| char::from_u32(cp))
.collect();
assert_eq!(text, "Hello, World!"); assert_eq!(text, "Hello, World!");
} }
@@ -1696,7 +1932,10 @@ mod tests {
parser.parse("Hello, 世界!".as_bytes(), &mut handler); parser.parse("Hello, 世界!".as_bytes(), &mut handler);
assert_eq!(handler.text_chunks.len(), 1); assert_eq!(handler.text_chunks.len(), 1);
let text: String = handler.text_chunks[0].iter().filter_map(|&cp| char::from_u32(cp)).collect(); let text: String = handler.text_chunks[0]
.iter()
.filter_map(|&cp| char::from_u32(cp))
.collect();
assert_eq!(text, "Hello, 世界!"); assert_eq!(text, "Hello, 世界!");
} }
@@ -1709,7 +1948,10 @@ mod tests {
parser.parse(b"Hello\nWorld\r!", &mut handler); parser.parse(b"Hello\nWorld\r!", &mut handler);
assert_eq!(handler.text_chunks.len(), 1); assert_eq!(handler.text_chunks.len(), 1);
let text: String = handler.text_chunks[0].iter().filter_map(|&cp| char::from_u32(cp)).collect(); let text: String = handler.text_chunks[0]
.iter()
.filter_map(|&cp| char::from_u32(cp))
.collect();
assert_eq!(text, "Hello\nWorld\r!"); assert_eq!(text, "Hello\nWorld\r!");
} }
@@ -1732,8 +1974,14 @@ mod tests {
parser.parse(b"Hello\x1b[1mWorld", &mut handler); parser.parse(b"Hello\x1b[1mWorld", &mut handler);
assert_eq!(handler.text_chunks.len(), 2); assert_eq!(handler.text_chunks.len(), 2);
let text1: String = handler.text_chunks[0].iter().filter_map(|&cp| char::from_u32(cp)).collect(); let text1: String = handler.text_chunks[0]
let text2: String = handler.text_chunks[1].iter().filter_map(|&cp| char::from_u32(cp)).collect(); .iter()
.filter_map(|&cp| char::from_u32(cp))
.collect();
let text2: String = handler.text_chunks[1]
.iter()
.filter_map(|&cp| char::from_u32(cp))
.collect();
assert_eq!(text1, "Hello"); assert_eq!(text1, "Hello");
assert_eq!(text2, "World"); assert_eq!(text2, "World");
assert_eq!(handler.csi_count, 1); assert_eq!(handler.csi_count, 1);