esc handling progress
This commit is contained in:
+63
-6
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-36
@@ -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,19 +2167,9 @@ 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 {
|
|
||||||
let bit = 1u64 << row_idx;
|
|
||||||
if (dirty_bitmap & bit) == 0 {
|
|
||||||
continue;
|
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 {
|
||||||
continue;
|
continue;
|
||||||
@@ -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
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+336
-88
@@ -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,8 +949,7 @@ 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;
|
||||||
@@ -814,10 +981,8 @@ impl SharedParser {
|
|||||||
log::debug!("Invalid CSI character: {:02x}", ch);
|
log::debug!("Invalid CSI character: {:02x}", ch);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
CsiState::Body => match ch {
|
||||||
CsiState::Body => {
|
|
||||||
match ch {
|
|
||||||
b'0'..=b'9' => {
|
b'0'..=b'9' => {
|
||||||
csi.add_digit(ch);
|
csi.add_digit(ch);
|
||||||
}
|
}
|
||||||
@@ -859,21 +1024,21 @@ impl SharedParser {
|
|||||||
log::debug!("Invalid CSI body character: {:02x}", ch);
|
log::debug!("Invalid CSI body character: {:02x}", ch);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
CsiState::PostSecondary => match ch {
|
||||||
CsiState::PostSecondary => {
|
|
||||||
match ch {
|
|
||||||
b'@'..=b'~' => {
|
b'@'..=b'~' => {
|
||||||
csi.final_char = ch;
|
csi.final_char = ch;
|
||||||
csi.is_valid = true;
|
csi.is_valid = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::debug!("Invalid CSI post-secondary character: {:02x}", ch);
|
log::debug!(
|
||||||
|
"Invalid CSI post-secondary character: {:02x}",
|
||||||
|
ch
|
||||||
|
);
|
||||||
return true;
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user