Compare commits
11 Commits
32ce278743
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c31166c087 | |||
| f07b1724d9 | |||
| 1bbd8fd1b9 | |||
| 3164873a66 | |||
| 7f4249dde9 | |||
| a56a2108ac | |||
| fe0a21bdb9 | |||
| bc552f6f0f | |||
| d2939de936 | |||
| 350be6611c | |||
| 494b684d58 |
@@ -3,3 +3,4 @@
|
|||||||
/pkg
|
/pkg
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
*.tar.*
|
*.tar.*
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
+7
-1
@@ -75,13 +75,19 @@ rustc-hash = "2"
|
|||||||
# Base64 decoding for OSC statusline
|
# Base64 decoding for OSC statusline
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# Pure-Rust git implementation (replaces git subprocess calls)
|
||||||
|
gix = { version = "0.84", features = ["max-performance", "parallel", "status", "blob-diff"] }
|
||||||
|
gix-diff = "0.64"
|
||||||
|
gix-status = "0.31"
|
||||||
|
gix-revwalk = "0.32"
|
||||||
|
|
||||||
# Image processing (Kitty graphics protocol)
|
# Image processing (Kitty graphics protocol)
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "gif"] }
|
image = { version = "0.25", default-features = false, features = ["png", "gif"] }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
|
|
||||||
# Video decoding for WebM support (video only, no audio)
|
# Video decoding for WebM support (video only, no audio)
|
||||||
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
||||||
ffmpeg-next = { version = "8.0", optional = true }
|
ffmpeg-next = { version = "8.1", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["webm"]
|
default = ["webm"]
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# Maintainer: Zach <zach@brohn.se>
|
|
||||||
pkgname=zterm
|
|
||||||
pkgver=0.1.0
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="A GPU-accelerated terminal emulator for Wayland"
|
|
||||||
arch=('x86_64')
|
|
||||||
url="https://github.com/Zacharias-Brohn/zterm"
|
|
||||||
license=('MIT')
|
|
||||||
depends=(
|
|
||||||
'fontconfig'
|
|
||||||
'freetype2'
|
|
||||||
'wayland'
|
|
||||||
'libxkbcommon'
|
|
||||||
'vulkan-icd-loader'
|
|
||||||
)
|
|
||||||
makedepends=('rust' 'cargo')
|
|
||||||
source=()
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "$startdir"
|
|
||||||
cargo build --release --features production
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cd "$startdir"
|
|
||||||
install -Dm755 "target/release/zterm" "$pkgdir/usr/bin/zterm"
|
|
||||||
install -Dm644 "zterm.terminfo" "$pkgdir/usr/share/zterm/zterm.terminfo"
|
|
||||||
|
|
||||||
# Compile and install terminfo
|
|
||||||
tic -x -o "$pkgdir/usr/share/terminfo" zterm.terminfo
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
use zterm::vt_parser::*;
|
||||||
|
fn main() {}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use zterm::terminal::Terminal;
|
use zterm::terminal::Terminal;
|
||||||
use zterm::vt_parser::Parser;
|
use zterm::vt_parser::Parser;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
const ASCII_PRINTABLE: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
const ASCII_PRINTABLE: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
||||||
const CONTROL_CHARS: &[u8] = b"\n\t";
|
const CONTROL_CHARS: &[u8] = b"\n\t";
|
||||||
|
|||||||
@@ -521,7 +521,6 @@ pub fn render_box_char(
|
|||||||
c: char,
|
c: char,
|
||||||
cell_width: usize,
|
cell_width: usize,
|
||||||
cell_height: usize,
|
cell_height: usize,
|
||||||
font_size: f32,
|
|
||||||
dpi: f64,
|
dpi: f64,
|
||||||
) -> Option<(Vec<u8>, bool)> {
|
) -> Option<(Vec<u8>, bool)> {
|
||||||
let w = cell_width;
|
let w = cell_width;
|
||||||
|
|||||||
@@ -387,7 +387,6 @@ impl Config {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
log::info!("No config file found at {:?}, creating with defaults", config_path);
|
|
||||||
let default_config = Self::default();
|
let default_config = Self::default();
|
||||||
if let Err(e) = default_config.save() {
|
if let Err(e) = default_config.save() {
|
||||||
log::warn!("Failed to write default config: {}", e);
|
log::warn!("Failed to write default config: {}", e);
|
||||||
@@ -398,7 +397,6 @@ impl Config {
|
|||||||
match fs::read_to_string(&config_path) {
|
match fs::read_to_string(&config_path) {
|
||||||
Ok(contents) => match serde_json::from_str(&contents) {
|
Ok(contents) => match serde_json::from_str(&contents) {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
log::info!("Loaded config from {:?}", config_path);
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -431,7 +429,6 @@ impl Config {
|
|||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
fs::write(&config_path, json)?;
|
fs::write(&config_path, json)?;
|
||||||
log::info!("Saved config to {:?}", config_path);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-19
@@ -214,19 +214,6 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
|
|||||||
// Try to use fontconfig to find the font family
|
// Try to use fontconfig to find the font family
|
||||||
if let Some(family) = font_family {
|
if let Some(family) = font_family {
|
||||||
let paths = find_font_family_variants(family);
|
let paths = find_font_family_variants(family);
|
||||||
log::info!("Font family '{}' resolved to:", family);
|
|
||||||
for (i, path) in paths.iter().enumerate() {
|
|
||||||
let style = match i {
|
|
||||||
0 => "Regular",
|
|
||||||
1 => "Bold",
|
|
||||||
2 => "Italic",
|
|
||||||
3 => "BoldItalic",
|
|
||||||
_ => "Unknown",
|
|
||||||
};
|
|
||||||
if let Some(p) = path {
|
|
||||||
log::info!(" {}: {:?}", style, p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the regular font (required)
|
// Load the regular font (required)
|
||||||
if let Some(regular_path) = &paths[0] {
|
if let Some(regular_path) = &paths[0] {
|
||||||
@@ -277,11 +264,7 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
|
|||||||
load_font_variant(std::path::Path::new(bold_italic)),
|
load_font_variant(std::path::Path::new(bold_italic)),
|
||||||
];
|
];
|
||||||
|
|
||||||
log::info!("Loaded font from fallback paths:");
|
|
||||||
log::info!(" Regular: {}", regular);
|
|
||||||
if variants[1].is_some() { log::info!(" Bold: {}", bold); }
|
|
||||||
if variants[2].is_some() { log::info!(" Italic: {}", italic); }
|
|
||||||
if variants[3].is_some() { log::info!(" BoldItalic: {}", bold_italic); }
|
|
||||||
|
|
||||||
return (font_data, primary_font, variants);
|
return (font_data, primary_font, variants);
|
||||||
}
|
}
|
||||||
@@ -293,7 +276,7 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
|
|||||||
let primary_font = regular_variant.clone_font();
|
let primary_font = regular_variant.clone_font();
|
||||||
let font_data = regular_variant.clone_data();
|
let font_data = regular_variant.clone_data();
|
||||||
let variants: [Option<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
let variants: [Option<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
||||||
log::info!("Loaded NotoSansMono as fallback");
|
|
||||||
return (font_data, primary_font, variants);
|
return (font_data, primary_font, variants);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ pub mod statusline;
|
|||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod simd_utf8;
|
pub mod simd_utf8;
|
||||||
pub mod vt_parser;
|
pub mod vt_parser;
|
||||||
|
mod vt_test_osc;
|
||||||
|
|||||||
+570
-289
File diff suppressed because it is too large
Load Diff
+76
-106
@@ -198,6 +198,7 @@ enum SpriteTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The terminal renderer.
|
/// The terminal renderer.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
device: wgpu::Device,
|
device: wgpu::Device,
|
||||||
@@ -1552,6 +1553,53 @@ impl Renderer {
|
|||||||
Some((col, row))
|
Some((col, row))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a pixel position to a terminal cell position relative to a specific pane.
|
||||||
|
/// `pane_x` and `pane_y` are the pane's pixel offset within the grid area.
|
||||||
|
/// `pane_width` and `pane_height` are the pane's dimensions in pixels.
|
||||||
|
/// `pane_cols` and `pane_rows` are the pane's dimensions in cells.
|
||||||
|
/// Returns None if the position is outside this pane's area.
|
||||||
|
pub fn pane_pixel_to_cell(
|
||||||
|
&self,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
pane_x: f32,
|
||||||
|
pane_y: f32,
|
||||||
|
pane_width: f32,
|
||||||
|
pane_height: f32,
|
||||||
|
pane_cols: usize,
|
||||||
|
pane_rows: usize,
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
|
let terminal_y_offset = self.terminal_y_offset();
|
||||||
|
let grid_x_offset = self.grid_x_offset();
|
||||||
|
let grid_y_offset = self.grid_y_offset();
|
||||||
|
|
||||||
|
// Convert pane pixel offset to screen pixel offset
|
||||||
|
let pane_screen_x = grid_x_offset + pane_x;
|
||||||
|
let pane_screen_y = terminal_y_offset + grid_y_offset + pane_y;
|
||||||
|
|
||||||
|
// Check if position is within this pane's screen bounds
|
||||||
|
if (x as f32) < pane_screen_x
|
||||||
|
|| (x as f32) >= pane_screen_x + pane_width
|
||||||
|
|| (y as f32) < pane_screen_y
|
||||||
|
|| (y as f32) >= pane_screen_y + pane_height
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate cell position relative to pane origin
|
||||||
|
let local_x = (x as f32) - pane_screen_x;
|
||||||
|
let local_y = (y as f32) - pane_screen_y;
|
||||||
|
|
||||||
|
let col = (local_x / self.cell_metrics.cell_width as f32).floor() as usize;
|
||||||
|
let row = (local_y / self.cell_metrics.cell_height as f32).floor() as usize;
|
||||||
|
|
||||||
|
if col >= pane_cols || row >= pane_rows {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((col, row))
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the scale factor and recalculates font/cell dimensions.
|
/// Updates the scale factor and recalculates font/cell dimensions.
|
||||||
/// Returns true if the cell dimensions changed (terminal needs resize).
|
/// Returns true if the cell dimensions changed (terminal needs resize).
|
||||||
pub fn set_scale_factor(&mut self, new_scale: f64) -> bool {
|
pub fn set_scale_factor(&mut self, new_scale: f64) -> bool {
|
||||||
@@ -1589,11 +1637,6 @@ impl Renderer {
|
|||||||
// Update the font units to pixels scale factor
|
// Update the font units to pixels scale factor
|
||||||
self.font_units_to_px = self.font_size / self.primary_font.height_unscaled();
|
self.font_units_to_px = self.font_size / self.primary_font.height_unscaled();
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Scale factor changed to {}: font {}px -> {}px, cell: {}x{}, baseline: {}",
|
|
||||||
new_scale, self.base_font_size, self.font_size, self.cell_metrics.cell_width, self.cell_metrics.cell_height, self.cell_metrics.baseline
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset atlas and all sprite/glyph caches (includes cursor sprite creation)
|
// Reset atlas and all sprite/glyph caches (includes cursor sprite creation)
|
||||||
self.reset_atlas();
|
self.reset_atlas();
|
||||||
|
|
||||||
@@ -1648,11 +1691,6 @@ impl Renderer {
|
|||||||
// Update the font units to pixels scale factor
|
// Update the font units to pixels scale factor
|
||||||
self.font_units_to_px = self.font_size / self.primary_font.height_unscaled();
|
self.font_units_to_px = self.font_size / self.primary_font.height_unscaled();
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Font size changed to {}px -> {}px, cell: {}x{}, baseline: {}",
|
|
||||||
size, self.font_size, self.cell_metrics.cell_width, self.cell_metrics.cell_height, self.cell_metrics.baseline
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset atlas and all sprite/glyph caches (includes cursor sprite creation)
|
// Reset atlas and all sprite/glyph caches (includes cursor sprite creation)
|
||||||
self.reset_atlas();
|
self.reset_atlas();
|
||||||
|
|
||||||
@@ -1666,7 +1704,7 @@ impl Renderer {
|
|||||||
/// NOTE: This should ONLY be called for font/scale changes, NOT when atlas is full
|
/// NOTE: This should ONLY be called for font/scale changes, NOT when atlas is full
|
||||||
/// (for that case, we add a new layer via add_atlas_layer()).
|
/// (for that case, we add a new layer via add_atlas_layer()).
|
||||||
fn reset_atlas(&mut self) {
|
fn reset_atlas(&mut self) {
|
||||||
log::info!("Resetting glyph atlas (font/scale changed)");
|
|
||||||
|
|
||||||
// Clear all glyph caches - they need to be re-rasterized at new size
|
// Clear all glyph caches - they need to be re-rasterized at new size
|
||||||
self.char_cache.clear();
|
self.char_cache.clear();
|
||||||
@@ -2141,6 +2179,21 @@ impl Renderer {
|
|||||||
self.pane_resources.retain(|id, _| active_pane_ids.contains(id));
|
self.pane_resources.retain(|id, _| active_pane_ids.contains(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Force a full GPU cell buffer rebuild on the next call to update_gpu_cells.
|
||||||
|
pub fn force_full_redraw(&mut self) {
|
||||||
|
self.cells_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a full redraw is pending.
|
||||||
|
pub fn has_pending_redraw(&self) -> bool {
|
||||||
|
self.cells_dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called after a render to clear the pending redraw flag.
|
||||||
|
pub fn clear_pending_redraw(&mut self) {
|
||||||
|
self.cells_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update GPU cell buffer from terminal content.
|
/// Update GPU cell buffer from terminal content.
|
||||||
/// Like Kitty, this only processes dirty lines to minimize work.
|
/// Like Kitty, this only processes dirty lines to minimize work.
|
||||||
///
|
///
|
||||||
@@ -2150,10 +2203,6 @@ 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 {
|
||||||
@@ -2162,14 +2211,17 @@ impl Renderer {
|
|||||||
self.cells_dirty = true;
|
self.cells_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this terminal has any dirty lines
|
||||||
|
let has_dirty = terminal.has_any_dirty_line();
|
||||||
|
|
||||||
// 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
|
||||||
// OPTIMIZATION: Only process dirty lines or when full rebuild is needed
|
// OPTIMIZATION: Only process dirty lines or when full rebuild is needed
|
||||||
// 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 or terminal has dirty lines)
|
||||||
if !self.cells_dirty && !terminal.is_line_dirty(row_idx) {
|
if !self.cells_dirty && !has_dirty && !terminal.is_line_dirty(row_idx) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2343,61 +2395,21 @@ 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
|
// Always update self.gpu_cells from the current terminal to avoid
|
||||||
|
// stale data from a previous pane being written to the wrong GPU buffer.
|
||||||
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 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() {
|
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;
|
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;
|
|
||||||
any_updated = true;
|
any_updated = true;
|
||||||
} else {
|
|
||||||
// Only update dirty lines - use is_line_dirty() which handles all 256 lines
|
|
||||||
for row_idx in 0..rows {
|
|
||||||
if terminal.is_line_dirty(row_idx) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2969,7 +2981,6 @@ impl Renderer {
|
|||||||
c,
|
c,
|
||||||
self.cell_metrics.cell_width as usize,
|
self.cell_metrics.cell_width as usize,
|
||||||
self.cell_metrics.cell_height as usize,
|
self.cell_metrics.cell_height as usize,
|
||||||
self.font_size,
|
|
||||||
self.dpi,
|
self.dpi,
|
||||||
) {
|
) {
|
||||||
// Box-drawing bitmaps are already cell-sized and fill from top-left.
|
// Box-drawing bitmaps are already cell-sized and fill from top-left.
|
||||||
@@ -3680,7 +3691,7 @@ impl Renderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Adding atlas layer {} (was on layer {})", new_layer, self.atlas_current_layer);
|
|
||||||
|
|
||||||
// Create real texture for the new layer (replacing the dummy)
|
// Create real texture for the new layer (replacing the dummy)
|
||||||
self.ensure_atlas_layer_capacity(new_layer);
|
self.ensure_atlas_layer_capacity(new_layer);
|
||||||
@@ -3707,7 +3718,7 @@ impl Renderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Adding atlas layer {} (replacing dummy texture)", target_layer);
|
|
||||||
|
|
||||||
// Create new real texture (8192x8192)
|
// Create new real texture (8192x8192)
|
||||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
@@ -4635,7 +4646,7 @@ impl Renderer {
|
|||||||
{
|
{
|
||||||
let update_time = t0.elapsed();
|
let update_time = t0.elapsed();
|
||||||
if update_time.as_micros() > 500 {
|
if update_time.as_micros() > 500 {
|
||||||
log::info!("update_gpu_cells took {:?}", update_time);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4679,35 +4690,6 @@ 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) {
|
||||||
@@ -4769,7 +4751,7 @@ impl Renderer {
|
|||||||
{
|
{
|
||||||
let pane_loop_time = pane_loop_start.elapsed();
|
let pane_loop_time = pane_loop_start.elapsed();
|
||||||
if pane_loop_time.as_micros() > 500 {
|
if pane_loop_time.as_micros() > 500 {
|
||||||
log::info!("pane_loop took {:?}", pane_loop_time);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5258,19 +5240,7 @@ impl Renderer {
|
|||||||
let after_submit = frame_start.elapsed();
|
let after_submit = frame_start.elapsed();
|
||||||
output.present();
|
output.present();
|
||||||
|
|
||||||
// Log timing if frame took more than 1ms (only with render_timing feature)
|
self.clear_pending_redraw();
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
{
|
|
||||||
let after_present = frame_start.elapsed();
|
|
||||||
if after_present.as_micros() > 1000 {
|
|
||||||
log::info!("render_panes: before_submit={:?} submit={:?} present={:?} total={:?}",
|
|
||||||
before_submit,
|
|
||||||
after_submit - before_submit,
|
|
||||||
after_present - after_submit,
|
|
||||||
after_present);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-104
@@ -298,87 +298,6 @@ struct AlternateScreen {
|
|||||||
scroll_bottom: usize,
|
scroll_bottom: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timing stats for performance debugging.
|
|
||||||
/// Only populated when the `render_timing` feature is enabled.
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ProcessingStats {
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Total time spent in scroll_up operations (nanoseconds).
|
|
||||||
pub scroll_up_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Number of scroll_up calls.
|
|
||||||
pub scroll_up_count: u32,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Total time spent in scrollback operations (nanoseconds).
|
|
||||||
pub scrollback_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Time in VecDeque pop_front.
|
|
||||||
pub pop_front_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Time in VecDeque push_back.
|
|
||||||
pub push_back_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Time in mem::swap.
|
|
||||||
pub swap_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Total time spent in line clearing (nanoseconds).
|
|
||||||
pub clear_line_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Total time spent in text handler (nanoseconds).
|
|
||||||
pub text_handler_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Total time spent in CSI handler (nanoseconds).
|
|
||||||
pub csi_handler_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Number of CSI sequences processed.
|
|
||||||
pub csi_count: u32,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Number of characters processed.
|
|
||||||
pub chars_processed: u32,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Total time spent in VT parser (consume_input) - nanoseconds.
|
|
||||||
pub vt_parser_ns: u64,
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
/// Number of consume_input calls.
|
|
||||||
pub consume_input_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessingStats {
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
*self = Self::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "render_timing"))]
|
|
||||||
pub fn reset(&mut self) {}
|
|
||||||
|
|
||||||
#[cfg(feature = "render_timing")]
|
|
||||||
pub fn log_if_slow(&self, threshold_ms: u64) {
|
|
||||||
let total_ms =
|
|
||||||
(self.scroll_up_ns + self.text_handler_ns + self.csi_handler_ns)
|
|
||||||
/ 1_000_000;
|
|
||||||
if total_ms >= threshold_ms {
|
|
||||||
let vt_only_ns = self
|
|
||||||
.vt_parser_ns
|
|
||||||
.saturating_sub(self.text_handler_ns + self.csi_handler_ns);
|
|
||||||
log::info!(
|
|
||||||
"[PARSE_DETAIL] text={:.2}ms ({}chars) csi={:.2}ms ({}x) vt_only={:.2}ms ({}calls) scroll={:.2}ms ({}x)",
|
|
||||||
self.text_handler_ns as f64 / 1_000_000.0,
|
|
||||||
self.chars_processed,
|
|
||||||
self.csi_handler_ns as f64 / 1_000_000.0,
|
|
||||||
self.csi_count,
|
|
||||||
vt_only_ns as f64 / 1_000_000.0,
|
|
||||||
self.consume_input_count,
|
|
||||||
self.scroll_up_ns as f64 / 1_000_000.0,
|
|
||||||
self.scroll_up_count,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "render_timing"))]
|
|
||||||
pub fn log_if_slow(&self, _threshold_ms: u64) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kitty-style ring buffer for scrollback history.
|
/// Kitty-style ring buffer for scrollback history.
|
||||||
///
|
///
|
||||||
/// Pre-allocates all lines upfront to avoid allocation during scrolling.
|
/// Pre-allocates all lines upfront to avoid allocation during scrolling.
|
||||||
@@ -569,7 +488,7 @@ pub struct Terminal {
|
|||||||
/// Synchronized output mode (for reducing flicker).
|
/// Synchronized output mode (for reducing flicker).
|
||||||
synchronized_output: bool,
|
synchronized_output: bool,
|
||||||
/// Performance timing stats (for debugging).
|
/// Performance timing stats (for debugging).
|
||||||
pub stats: ProcessingStats,
|
|
||||||
/// Command queue for terminal-to-application communication.
|
/// Command queue for terminal-to-application communication.
|
||||||
/// Commands are added by OSC handlers and consumed by the application.
|
/// Commands are added by OSC handlers and consumed by the application.
|
||||||
command_queue: Vec<TerminalCommand>,
|
command_queue: Vec<TerminalCommand>,
|
||||||
@@ -587,12 +506,6 @@ impl Terminal {
|
|||||||
|
|
||||||
/// Creates a new terminal with the given dimensions and scrollback limit.
|
/// Creates a new terminal with the given dimensions and scrollback limit.
|
||||||
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
|
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
|
||||||
log::info!(
|
|
||||||
"Terminal::new: cols={}, rows={}, scroll_bottom={}",
|
|
||||||
cols,
|
|
||||||
rows,
|
|
||||||
rows.saturating_sub(1)
|
|
||||||
);
|
|
||||||
let grid = vec![vec![Cell::default(); cols]; rows];
|
let grid = vec![vec![Cell::default(); cols]; rows];
|
||||||
let line_map: Vec<usize> = (0..rows).collect();
|
let line_map: Vec<usize> = (0..rows).collect();
|
||||||
|
|
||||||
@@ -632,7 +545,7 @@ impl Terminal {
|
|||||||
bracketed_paste: false,
|
bracketed_paste: false,
|
||||||
focus_reporting: false,
|
focus_reporting: false,
|
||||||
synchronized_output: false,
|
synchronized_output: false,
|
||||||
stats: ProcessingStats::default(),
|
|
||||||
command_queue: Vec::new(),
|
command_queue: Vec::new(),
|
||||||
image_storage: ImageStorage::new(),
|
image_storage: ImageStorage::new(),
|
||||||
cell_width: 10.0, // Default, will be set by renderer
|
cell_width: 10.0, // Default, will be set by renderer
|
||||||
@@ -668,6 +581,12 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if any line is dirty.
|
||||||
|
#[inline]
|
||||||
|
pub fn has_any_dirty_line(&self) -> bool {
|
||||||
|
self.dirty_lines[0] != 0 || self.dirty_lines[1] != 0 || self.dirty_lines[2] != 0 || self.dirty_lines[3] != 0
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear all dirty line flags.
|
/// Clear all dirty line flags.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn clear_dirty_lines(&mut self) {
|
pub fn clear_dirty_lines(&mut self) {
|
||||||
@@ -789,14 +708,6 @@ impl Terminal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"Terminal::resize: {}x{} -> {}x{}",
|
|
||||||
self.cols,
|
|
||||||
self.rows,
|
|
||||||
cols,
|
|
||||||
rows
|
|
||||||
);
|
|
||||||
|
|
||||||
let old_cols = self.cols;
|
let old_cols = self.cols;
|
||||||
let old_rows = self.rows;
|
let old_rows = self.rows;
|
||||||
|
|
||||||
@@ -1407,7 +1318,6 @@ impl Handler for Terminal {
|
|||||||
// Look for patterns like "38;2;128" or "4;64;64m" - these are SGR parameters
|
// Look for patterns like "38;2;128" or "4;64;64m" - these are SGR parameters
|
||||||
if codepoints.len() >= 3 {
|
if codepoints.len() >= 3 {
|
||||||
let has_semicolon = codepoints.iter().any(|&c| c == 0x3B); // ';'
|
let has_semicolon = codepoints.iter().any(|&c| c == 0x3B); // ';'
|
||||||
let has_m = codepoints.iter().any(|&c| c == 0x6D); // 'm'
|
|
||||||
let mostly_digits = codepoints
|
let mostly_digits = codepoints
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&&c| c >= 0x30 && c <= 0x39)
|
.filter(|&&c| c >= 0x30 && c <= 0x39)
|
||||||
@@ -1533,16 +1443,19 @@ impl Handler for Terminal {
|
|||||||
if self.cursor_col > 0 {
|
if self.cursor_col > 0 {
|
||||||
self.cursor_col -= 1;
|
self.cursor_col -= 1;
|
||||||
}
|
}
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
0x09 => {
|
0x09 => {
|
||||||
let next_tab = (self.cursor_col / 8 + 1) * 8;
|
let next_tab = (self.cursor_col / 8 + 1) * 8;
|
||||||
self.cursor_col = next_tab.min(self.cols - 1);
|
self.cursor_col = next_tab.min(self.cols - 1);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
0x0A | 0x0B | 0x0C => {
|
0x0A | 0x0B | 0x0C => {
|
||||||
self.advance_row();
|
self.advance_row();
|
||||||
}
|
}
|
||||||
0x0D => {
|
0x0D => {
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1692,12 +1605,6 @@ impl Handler for Terminal {
|
|||||||
|
|
||||||
let statusline =
|
let statusline =
|
||||||
content.filter(|s| !s.is_empty());
|
content.filter(|s| !s.is_empty());
|
||||||
log::info!(
|
|
||||||
"OSC 51: Set statusline: {:?}",
|
|
||||||
statusline
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| format!("{} bytes", s.len()))
|
|
||||||
);
|
|
||||||
self.command_queue.push(
|
self.command_queue.push(
|
||||||
TerminalCommand::SetStatusline(statusline),
|
TerminalCommand::SetStatusline(statusline),
|
||||||
);
|
);
|
||||||
@@ -1808,6 +1715,7 @@ impl Handler for Terminal {
|
|||||||
if self.origin_mode { self.scroll_top } else { 0 };
|
if self.origin_mode { self.scroll_top } else { 0 };
|
||||||
self.cursor_row =
|
self.cursor_row =
|
||||||
self.cursor_row.saturating_sub(n).max(min_row);
|
self.cursor_row.saturating_sub(n).max(min_row);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
'B' => {
|
'B' => {
|
||||||
let n = params.get(0, 1).max(1) as usize;
|
let n = params.get(0, 1).max(1) as usize;
|
||||||
@@ -1817,6 +1725,7 @@ impl Handler for Terminal {
|
|||||||
self.rows - 1
|
self.rows - 1
|
||||||
};
|
};
|
||||||
self.cursor_row = (self.cursor_row + n).min(max_row);
|
self.cursor_row = (self.cursor_row + n).min(max_row);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
// Cursor Forward
|
// Cursor Forward
|
||||||
'C' => {
|
'C' => {
|
||||||
@@ -1829,11 +1738,13 @@ impl Handler for Terminal {
|
|||||||
old_col,
|
old_col,
|
||||||
self.cursor_col
|
self.cursor_col
|
||||||
);
|
);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
// Cursor Back
|
// Cursor Back
|
||||||
'D' => {
|
'D' => {
|
||||||
let n = params.get(0, 1).max(1) as usize;
|
let n = params.get(0, 1).max(1) as usize;
|
||||||
self.cursor_col = self.cursor_col.saturating_sub(n);
|
self.cursor_col = self.cursor_col.saturating_sub(n);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
'E' => {
|
'E' => {
|
||||||
let n = params.get(0, 1).max(1) as usize;
|
let n = params.get(0, 1).max(1) as usize;
|
||||||
@@ -1844,6 +1755,7 @@ impl Handler for Terminal {
|
|||||||
};
|
};
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
self.cursor_row = (self.cursor_row + n).min(max_row);
|
self.cursor_row = (self.cursor_row + n).min(max_row);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
'F' => {
|
'F' => {
|
||||||
let n = params.get(0, 1).max(1) as usize;
|
let n = params.get(0, 1).max(1) as usize;
|
||||||
@@ -1852,6 +1764,7 @@ impl Handler for Terminal {
|
|||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
self.cursor_row =
|
self.cursor_row =
|
||||||
self.cursor_row.saturating_sub(n).max(min_row);
|
self.cursor_row.saturating_sub(n).max(min_row);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
// Cursor Horizontal Absolute (CHA)
|
// Cursor Horizontal Absolute (CHA)
|
||||||
'G' => {
|
'G' => {
|
||||||
@@ -1863,6 +1776,7 @@ impl Handler for Terminal {
|
|||||||
self.cursor_col,
|
self.cursor_col,
|
||||||
old_col
|
old_col
|
||||||
);
|
);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
// Cursor Position
|
// Cursor Position
|
||||||
'H' | 'f' => {
|
'H' | 'f' => {
|
||||||
@@ -1876,6 +1790,7 @@ impl Handler for Terminal {
|
|||||||
self.cursor_row = (row - 1).min(self.rows - 1);
|
self.cursor_row = (row - 1).min(self.rows - 1);
|
||||||
}
|
}
|
||||||
self.cursor_col = (col - 1).min(self.cols - 1);
|
self.cursor_col = (col - 1).min(self.cols - 1);
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
// Erase in Display
|
// Erase in Display
|
||||||
'J' => {
|
'J' => {
|
||||||
@@ -2273,6 +2188,7 @@ impl Handler for Terminal {
|
|||||||
} else {
|
} else {
|
||||||
self.cursor_row += 1;
|
self.cursor_row += 1;
|
||||||
}
|
}
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn newline(&mut self) {
|
fn newline(&mut self) {
|
||||||
@@ -2282,6 +2198,7 @@ impl Handler for Terminal {
|
|||||||
} else {
|
} else {
|
||||||
self.cursor_row += 1;
|
self.cursor_row += 1;
|
||||||
}
|
}
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reverse_index(&mut self) {
|
fn reverse_index(&mut self) {
|
||||||
@@ -2290,6 +2207,7 @@ impl Handler for Terminal {
|
|||||||
} else {
|
} else {
|
||||||
self.cursor_row -= 1;
|
self.cursor_row -= 1;
|
||||||
}
|
}
|
||||||
|
self.mark_line_dirty(self.cursor_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_tab_stop(&mut self) {
|
fn set_tab_stop(&mut self) {
|
||||||
|
|||||||
+64
-14
@@ -482,7 +482,9 @@ impl SharedParser {
|
|||||||
|
|
||||||
// Like Kitty line 1516: consume_input(self, ...)
|
// Like Kitty line 1516: consume_input(self, ...)
|
||||||
let made_progress = self.consume_input(handler);
|
let made_progress = self.consume_input(handler);
|
||||||
|
if made_progress {
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-acquire lock
|
// Re-acquire lock
|
||||||
state = self.state.lock().unwrap();
|
state = self.state.lock().unwrap();
|
||||||
@@ -635,14 +637,14 @@ impl SharedParser {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
State::Csi => {
|
State::Csi => {
|
||||||
// Like Kitty lines 1465-1466:
|
let state_before = *vte_state;
|
||||||
// if (consume_csi(self)) { self->read.consumed = self->read.pos; if (self->csi.is_valid) dispatch_csi(self); SET_STATE(NORMAL); }
|
|
||||||
if Self::consume_csi_impl(
|
if Self::consume_csi_impl(
|
||||||
handler,
|
handler,
|
||||||
buf,
|
buf,
|
||||||
parse_pos,
|
parse_pos,
|
||||||
parse_sz,
|
parse_sz,
|
||||||
*parse_consumed,
|
parse_consumed,
|
||||||
|
vte_state,
|
||||||
csi,
|
csi,
|
||||||
escape_len,
|
escape_len,
|
||||||
) {
|
) {
|
||||||
@@ -650,30 +652,41 @@ impl SharedParser {
|
|||||||
if csi.is_valid {
|
if csi.is_valid {
|
||||||
handler.csi(csi);
|
handler.csi(csi);
|
||||||
}
|
}
|
||||||
|
if *vte_state == state_before {
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
*vte_state != state_before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Osc => {
|
State::Osc => {
|
||||||
|
let state_before = *vte_state;
|
||||||
if Self::consume_osc_impl(
|
if Self::consume_osc_impl(
|
||||||
handler, buf, parse_pos, parse_sz, vte_state, osc_buffer,
|
handler,
|
||||||
|
buf,
|
||||||
|
parse_pos,
|
||||||
|
parse_sz,
|
||||||
|
parse_consumed,
|
||||||
|
vte_state,
|
||||||
|
osc_buffer,
|
||||||
escape_len,
|
escape_len,
|
||||||
) {
|
) {
|
||||||
*parse_consumed = *parse_pos;
|
*parse_consumed = *parse_pos;
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
*vte_state != state_before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Dcs | State::Apc | State::Pm | State::Sos => {
|
State::Dcs | State::Apc | State::Pm | State::Sos => {
|
||||||
|
let state_before = *vte_state;
|
||||||
if Self::consume_string_impl(
|
if Self::consume_string_impl(
|
||||||
handler,
|
handler,
|
||||||
buf,
|
buf,
|
||||||
parse_pos,
|
parse_pos,
|
||||||
parse_sz,
|
parse_sz,
|
||||||
|
parse_consumed,
|
||||||
vte_state,
|
vte_state,
|
||||||
string_buffer,
|
string_buffer,
|
||||||
escape_len,
|
escape_len,
|
||||||
@@ -682,7 +695,7 @@ impl SharedParser {
|
|||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
*vte_state != state_before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -837,6 +850,11 @@ impl SharedParser {
|
|||||||
b'\\' => {
|
b'\\' => {
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
}
|
}
|
||||||
|
0x1B => {
|
||||||
|
// ESC followed by ESC. Start new escape sequence.
|
||||||
|
*vte_state = State::Escape;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
@@ -913,7 +931,8 @@ impl SharedParser {
|
|||||||
buf: &[u8; BUF_SIZE],
|
buf: &[u8; BUF_SIZE],
|
||||||
parse_pos: &mut usize,
|
parse_pos: &mut usize,
|
||||||
parse_sz: usize,
|
parse_sz: usize,
|
||||||
parse_consumed: usize,
|
parse_consumed: &mut usize,
|
||||||
|
vte_state: &mut State,
|
||||||
csi: &mut CsiParams,
|
csi: &mut CsiParams,
|
||||||
escape_len: &mut usize,
|
escape_len: &mut usize,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -922,8 +941,15 @@ impl SharedParser {
|
|||||||
*parse_pos += 1;
|
*parse_pos += 1;
|
||||||
*escape_len += 1;
|
*escape_len += 1;
|
||||||
|
|
||||||
|
if ch == 0x1B {
|
||||||
|
*vte_state = State::Escape;
|
||||||
|
*parse_consumed = *parse_pos;
|
||||||
|
*escape_len = 0;
|
||||||
|
return false; // Aborted by new ESC
|
||||||
|
}
|
||||||
|
|
||||||
// Handle embedded control characters
|
// Handle embedded control characters
|
||||||
if ch <= 0x1F && ch != 0x1B {
|
if ch <= 0x1F {
|
||||||
handler.control(ch);
|
handler.control(ch);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1049,7 @@ impl SharedParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check max length
|
// Check max length
|
||||||
if *parse_pos - parse_consumed > MAX_ESCAPE_LEN {
|
if *parse_pos - *parse_consumed > MAX_ESCAPE_LEN {
|
||||||
log::debug!("CSI escape too long, ignoring");
|
log::debug!("CSI escape too long, ignoring");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1037,6 +1063,7 @@ impl SharedParser {
|
|||||||
buf: &[u8; BUF_SIZE],
|
buf: &[u8; BUF_SIZE],
|
||||||
parse_pos: &mut usize,
|
parse_pos: &mut usize,
|
||||||
parse_sz: usize,
|
parse_sz: usize,
|
||||||
|
parse_consumed: &mut usize,
|
||||||
vte_state: &mut State,
|
vte_state: &mut State,
|
||||||
osc_buffer: &mut Vec<u8>,
|
osc_buffer: &mut Vec<u8>,
|
||||||
escape_len: &mut usize,
|
escape_len: &mut usize,
|
||||||
@@ -1069,6 +1096,7 @@ impl SharedParser {
|
|||||||
*parse_pos += 1;
|
*parse_pos += 1;
|
||||||
handler.osc(osc_buffer);
|
handler.osc(osc_buffer);
|
||||||
*vte_state = State::Escape;
|
*vte_state = State::Escape;
|
||||||
|
*parse_consumed = *parse_pos;
|
||||||
*escape_len = 0;
|
*escape_len = 0;
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -1098,6 +1126,7 @@ impl SharedParser {
|
|||||||
buf: &[u8; BUF_SIZE],
|
buf: &[u8; BUF_SIZE],
|
||||||
parse_pos: &mut usize,
|
parse_pos: &mut usize,
|
||||||
parse_sz: usize,
|
parse_sz: usize,
|
||||||
|
parse_consumed: &mut usize,
|
||||||
vte_state: &mut State,
|
vte_state: &mut State,
|
||||||
string_buffer: &mut Vec<u8>,
|
string_buffer: &mut Vec<u8>,
|
||||||
escape_len: &mut usize,
|
escape_len: &mut usize,
|
||||||
@@ -1128,10 +1157,17 @@ impl SharedParser {
|
|||||||
);
|
);
|
||||||
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 \ - abort string, start new escape
|
||||||
string_buffer.push(ch);
|
|
||||||
*parse_pos += 1;
|
*parse_pos += 1;
|
||||||
*escape_len += 1;
|
Self::dispatch_string_command(
|
||||||
|
handler,
|
||||||
|
vte_state,
|
||||||
|
string_buffer,
|
||||||
|
);
|
||||||
|
*vte_state = State::Escape;
|
||||||
|
*parse_consumed = *parse_pos;
|
||||||
|
*escape_len = 0;
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// ESC at end of buffer - need more data
|
// ESC at end of buffer - need more data
|
||||||
return false;
|
return false;
|
||||||
@@ -1368,6 +1404,11 @@ impl Parser {
|
|||||||
self.state = State::Normal;
|
self.state = State::Normal;
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
0x1B => {
|
||||||
|
// ESC followed by ESC. Start new escape sequence.
|
||||||
|
self.state = State::Escape;
|
||||||
|
1
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Unknown escape sequence, ignore and return to normal
|
// Unknown escape sequence, ignore and return to normal
|
||||||
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
||||||
@@ -1442,8 +1483,17 @@ impl Parser {
|
|||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ch == 0x1B {
|
||||||
|
self.state = State::Escape;
|
||||||
|
self.escape_len = 0;
|
||||||
|
// Wait! If it's ESC, we consumed it. But the next loop in parse() will call consume_escape,
|
||||||
|
// which EXPECTS the char AFTER ESC.
|
||||||
|
// So consuming it is PERFECT!
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle control characters embedded in CSI (common to all states)
|
// Handle control characters embedded in CSI (common to all states)
|
||||||
if ch <= 0x1F && ch != 0x1B {
|
if ch <= 0x1F {
|
||||||
handler.control(ch);
|
handler.control(ch);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use crate::vt_parser::*;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct DummyHandler {
|
||||||
|
pub text: String,
|
||||||
|
pub osc_calls: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
impl Handler for DummyHandler {
|
||||||
|
fn text(&mut self, codepoints: &[u32]) {
|
||||||
|
for &c in codepoints {
|
||||||
|
if let Some(ch) = char::from_u32(c) {
|
||||||
|
self.text.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn control(&mut self, _byte: u8) {}
|
||||||
|
fn csi(&mut self, _csi: &CsiParams) {}
|
||||||
|
fn osc(&mut self, osc: &[u8]) {
|
||||||
|
self.osc_calls.push(osc.to_vec());
|
||||||
|
}
|
||||||
|
fn save_cursor(&mut self) {}
|
||||||
|
fn restore_cursor(&mut self) {}
|
||||||
|
fn reset(&mut self) {}
|
||||||
|
fn index(&mut self) {}
|
||||||
|
fn newline(&mut self) {}
|
||||||
|
fn reverse_index(&mut self) {}
|
||||||
|
fn screen_alignment(&mut self) {}
|
||||||
|
fn set_tab_stop(&mut self) {}
|
||||||
|
fn set_keypad_mode(&mut self, _mode: bool) {}
|
||||||
|
fn add_vt_parser_ns(&mut self, _ns: u64) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_osc_leak_byte_by_byte() {
|
||||||
|
let parser = SharedParser::new();
|
||||||
|
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() };
|
||||||
|
|
||||||
|
let data = b"\x1b]4;1;#769E00\x1b\\\x1b]4;2;#93DE88\x1b\\";
|
||||||
|
|
||||||
|
for &byte in data {
|
||||||
|
let (ptr, _) = parser.create_write_buffer();
|
||||||
|
unsafe {
|
||||||
|
*ptr = byte;
|
||||||
|
}
|
||||||
|
parser.commit_write(1);
|
||||||
|
|
||||||
|
while parser.run_parse_pass(&mut handler) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("TEXT: {:?}", handler.text);
|
||||||
|
println!("OSC calls: {}", handler.osc_calls.len());
|
||||||
|
for call in &handler.osc_calls {
|
||||||
|
println!(" OSC: {:?}", std::str::from_utf8(call).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!");
|
||||||
|
assert_eq!(handler.osc_calls.len(), 2, "Should have parsed exactly two OSC calls");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_csi_aborted_by_osc() {
|
||||||
|
let parser = SharedParser::new();
|
||||||
|
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() };
|
||||||
|
|
||||||
|
// An incomplete CSI sequence aborted by an OSC sequence
|
||||||
|
let data = b"\x1b[38;2;255;0;0\x1b]4;1;#769E00\x1b\\";
|
||||||
|
|
||||||
|
let (ptr, len) = parser.create_write_buffer();
|
||||||
|
assert!(len >= data.len());
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
|
||||||
|
}
|
||||||
|
parser.commit_write(data.len());
|
||||||
|
|
||||||
|
while parser.run_parse_pass(&mut handler) {}
|
||||||
|
|
||||||
|
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!");
|
||||||
|
assert_eq!(handler.osc_calls.len(), 1, "Should have parsed the OSC sequence even after aborting CSI");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user