Compare commits
13 Commits
f38033c782
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c31166c087 | |||
| f07b1724d9 | |||
| 1bbd8fd1b9 | |||
| 3164873a66 | |||
| 7f4249dde9 | |||
| a56a2108ac | |||
| fe0a21bdb9 | |||
| bc552f6f0f | |||
| d2939de936 | |||
| 350be6611c | |||
| 494b684d58 | |||
| 32ce278743 | |||
| 5129612f9f |
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -459,11 +459,16 @@ fn vs_cell_bg(
|
|||||||
|
|
||||||
// For default background (type 0), use fully transparent so the window's
|
// For default background (type 0), use fully transparent so the window's
|
||||||
// clear color (which has background_opacity applied) shows through.
|
// clear color (which has background_opacity applied) shows through.
|
||||||
|
// UNLESS the grid params specify an opaque background (e.g. alternate screen).
|
||||||
// Only non-default backgrounds should be opaque.
|
// Only non-default backgrounds should be opaque.
|
||||||
// But NOT if the cell is selected (selection always has white bg)
|
// But NOT if the cell is selected (selection always has white bg)
|
||||||
let bg_type = cell.bg & 0xFFu;
|
let bg_type = cell.bg & 0xFFu;
|
||||||
if bg_type == COLOR_TYPE_DEFAULT && !is_reverse && !is_selected {
|
if bg_type == COLOR_TYPE_DEFAULT && !is_reverse && !is_selected {
|
||||||
bg.a = 0.0;
|
if grid_params.background_opacity < 1.0 {
|
||||||
|
bg.a = 0.0;
|
||||||
|
} else {
|
||||||
|
bg.a = 1.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate cursor color
|
// Calculate cursor color
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+741
-378
File diff suppressed because it is too large
Load Diff
+156
-154
@@ -13,7 +13,7 @@ use crate::gpu_types::{
|
|||||||
EdgeGlowUniforms, QuadParams, StatuslineParams,
|
EdgeGlowUniforms, QuadParams, StatuslineParams,
|
||||||
ATLAS_SIZE, MAX_ATLAS_LAYERS, ATLAS_BPP, MAX_EDGE_GLOWS,
|
ATLAS_SIZE, MAX_ATLAS_LAYERS, ATLAS_BPP, MAX_EDGE_GLOWS,
|
||||||
COLOR_TYPE_DEFAULT, COLOR_TYPE_INDEXED, COLOR_TYPE_RGB,
|
COLOR_TYPE_DEFAULT, COLOR_TYPE_INDEXED, COLOR_TYPE_RGB,
|
||||||
ATTR_BOLD, ATTR_ITALIC, ATTR_STRIKE,
|
ATTR_BOLD, ATTR_ITALIC, ATTR_STRIKE, ATTR_REVERSE,
|
||||||
COLORED_GLYPH_FLAG,
|
COLORED_GLYPH_FLAG,
|
||||||
CURSOR_SPRITE_BEAM, CURSOR_SPRITE_UNDERLINE, CURSOR_SPRITE_HOLLOW,
|
CURSOR_SPRITE_BEAM, CURSOR_SPRITE_UNDERLINE, CURSOR_SPRITE_HOLLOW,
|
||||||
DECORATION_SPRITE_STRIKETHROUGH, DECORATION_SPRITE_UNDERLINE, DECORATION_SPRITE_DOUBLE_UNDERLINE,
|
DECORATION_SPRITE_STRIKETHROUGH, DECORATION_SPRITE_UNDERLINE, DECORATION_SPRITE_DOUBLE_UNDERLINE,
|
||||||
@@ -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();
|
||||||
@@ -1718,11 +1756,12 @@ impl Renderer {
|
|||||||
/// Pack cell attributes into u32 format for GPU.
|
/// Pack cell attributes into u32 format for GPU.
|
||||||
/// underline_style: 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed
|
/// underline_style: 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed
|
||||||
#[inline]
|
#[inline]
|
||||||
fn pack_attrs(bold: bool, italic: bool, underline_style: u8, strikethrough: bool) -> u32 {
|
fn pack_attrs(bold: bool, italic: bool, underline_style: u8, strikethrough: bool, reverse: bool) -> u32 {
|
||||||
let mut attrs = (underline_style as u32) & 0x7; // 3 bits for decoration type
|
let mut attrs = (underline_style as u32) & 0x7; // 3 bits for decoration type
|
||||||
if bold { attrs |= ATTR_BOLD; }
|
if bold { attrs |= ATTR_BOLD; }
|
||||||
if italic { attrs |= ATTR_ITALIC; }
|
if italic { attrs |= ATTR_ITALIC; }
|
||||||
if strikethrough { attrs |= ATTR_STRIKE; }
|
if strikethrough { attrs |= ATTR_STRIKE; }
|
||||||
|
if reverse { attrs |= ATTR_REVERSE; }
|
||||||
attrs
|
attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1899,7 +1938,7 @@ impl Renderer {
|
|||||||
bg: Self::pack_color(&cell.bg_color),
|
bg: Self::pack_color(&cell.bg_color),
|
||||||
decoration_fg: 0,
|
decoration_fg: 0,
|
||||||
sprite_idx: 0, // No glyph for continuation
|
sprite_idx: 0, // No glyph for continuation
|
||||||
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough),
|
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough, cell.reverse),
|
||||||
};
|
};
|
||||||
col += 1;
|
col += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -1972,7 +2011,7 @@ impl Renderer {
|
|||||||
bg: Self::pack_color(¤t_cell.bg_color),
|
bg: Self::pack_color(¤t_cell.bg_color),
|
||||||
decoration_fg: 0,
|
decoration_fg: 0,
|
||||||
sprite_idx: final_sprite_idx,
|
sprite_idx: final_sprite_idx,
|
||||||
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough),
|
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough, cell.reverse),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2023,7 +2062,7 @@ impl Renderer {
|
|||||||
bg: Self::pack_color(¤t_cell.bg_color),
|
bg: Self::pack_color(¤t_cell.bg_color),
|
||||||
decoration_fg: 0,
|
decoration_fg: 0,
|
||||||
sprite_idx: sprite_idx | COLORED_GLYPH_FLAG,
|
sprite_idx: sprite_idx | COLORED_GLYPH_FLAG,
|
||||||
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough),
|
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough, cell.reverse),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2052,7 +2091,7 @@ impl Renderer {
|
|||||||
bg: Self::pack_color(&cell.bg_color),
|
bg: Self::pack_color(&cell.bg_color),
|
||||||
decoration_fg: 0,
|
decoration_fg: 0,
|
||||||
sprite_idx,
|
sprite_idx,
|
||||||
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough),
|
attrs: Self::pack_attrs(cell.bold, cell.italic, cell.underline_style, cell.strikethrough, cell.reverse),
|
||||||
};
|
};
|
||||||
col += 1;
|
col += 1;
|
||||||
}
|
}
|
||||||
@@ -2140,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.
|
||||||
///
|
///
|
||||||
@@ -2149,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 {
|
||||||
@@ -2161,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2342,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
|
for row_idx in 0..rows {
|
||||||
static DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
if let Some(row) = terminal.get_visible_row(row_idx) {
|
||||||
let frame_num = DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
let start = row_idx * cols;
|
||||||
if frame_num % 60 == 0 { // Log every 60 frames (~1 second at 60fps)
|
let end = start + cols;
|
||||||
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 end > self.gpu_cells.len() {
|
||||||
}
|
continue;
|
||||||
|
|
||||||
// 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 {
|
|
||||||
if let Some(row) = terminal.get_visible_row(row_idx) {
|
|
||||||
let start = row_idx * 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_dirty = false;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map);
|
||||||
|
any_updated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2405,14 +2418,19 @@ impl Renderer {
|
|||||||
|
|
||||||
/// Parse ANSI escape sequences from raw statusline content.
|
/// Parse ANSI escape sequences from raw statusline content.
|
||||||
/// Returns a vector of (char, fg_color, bg_color, bold) tuples.
|
/// Returns a vector of (char, fg_color, bg_color, bold) tuples.
|
||||||
fn parse_ansi_statusline(content: &str) -> Vec<(char, StatuslineColor, StatuslineColor, bool)> {
|
fn parse_ansi_statusline(content: &str, is_light: bool) -> Vec<(char, StatuslineColor, StatuslineColor, bool)> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let chars: Vec<char> = content.chars().collect();
|
let chars: Vec<char> = content.chars().collect();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
||||||
// Current styling state
|
// Current styling state
|
||||||
let mut fg = StatuslineColor::Default;
|
let mut fg = StatuslineColor::Default;
|
||||||
let mut bg = StatuslineColor::Rgb(0x1a, 0x1a, 0x1a); // Default statusline background
|
let default_bg_color = if is_light {
|
||||||
|
StatuslineColor::Rgb(0xD0, 0xD0, 0xD0)
|
||||||
|
} else {
|
||||||
|
StatuslineColor::Rgb(0x1a, 0x1a, 0x1a)
|
||||||
|
};
|
||||||
|
let mut bg = default_bg_color.clone(); // Default statusline background
|
||||||
let mut bold = false;
|
let mut bold = false;
|
||||||
|
|
||||||
while i < chars.len() {
|
while i < chars.len() {
|
||||||
@@ -2493,7 +2511,7 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
49 => bg = StatuslineColor::Rgb(0x1a, 0x1a, 0x1a), // Reset to default statusline bg
|
49 => bg = default_bg_color.clone(), // Reset to default statusline bg
|
||||||
90..=97 => fg = StatuslineColor::Indexed((code - 90 + 8) as u8),
|
90..=97 => fg = StatuslineColor::Indexed((code - 90 + 8) as u8),
|
||||||
100..=107 => bg = StatuslineColor::Indexed((code - 100 + 8) as u8),
|
100..=107 => bg = StatuslineColor::Indexed((code - 100 + 8) as u8),
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -2527,7 +2545,7 @@ impl Renderer {
|
|||||||
/// this is used to expand the middle gap to fill the full window width.
|
/// this is used to expand the middle gap to fill the full window width.
|
||||||
///
|
///
|
||||||
/// Returns the number of columns used.
|
/// Returns the number of columns used.
|
||||||
fn update_statusline_cells(&mut self, content: &StatuslineContent, target_width: f32) -> usize {
|
fn update_statusline_cells(&mut self, content: &StatuslineContent, target_width: f32, is_light: bool) -> usize {
|
||||||
self.statusline_gpu_cells.clear();
|
self.statusline_gpu_cells.clear();
|
||||||
|
|
||||||
// Calculate target columns based on window width
|
// Calculate target columns based on window width
|
||||||
@@ -2539,14 +2557,19 @@ impl Renderer {
|
|||||||
self.statusline_max_cols
|
self.statusline_max_cols
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default background color for statusline (dark gray)
|
// Default background color for statusline
|
||||||
let default_bg = Self::pack_statusline_color(StatuslineColor::Rgb(0x1a, 0x1a, 0x1a));
|
let default_bg_color = if is_light {
|
||||||
|
StatuslineColor::Rgb(0xD0, 0xD0, 0xD0)
|
||||||
|
} else {
|
||||||
|
StatuslineColor::Rgb(0x1a, 0x1a, 0x1a)
|
||||||
|
};
|
||||||
|
let default_bg = Self::pack_statusline_color(default_bg_color);
|
||||||
let _ = default_bg; // Silence unused warning - used by Sections path
|
let _ = default_bg; // Silence unused warning - used by Sections path
|
||||||
|
|
||||||
match content {
|
match content {
|
||||||
StatuslineContent::Raw(ansi_content) => {
|
StatuslineContent::Raw(ansi_content) => {
|
||||||
// Parse ANSI escape sequences to extract colors and text
|
// Parse ANSI escape sequences to extract colors and text
|
||||||
let parsed = Self::parse_ansi_statusline(ansi_content);
|
let parsed = Self::parse_ansi_statusline(ansi_content, is_light);
|
||||||
|
|
||||||
// Find the middle gap (largest consecutive run of spaces)
|
// Find the middle gap (largest consecutive run of spaces)
|
||||||
// and expand it to fill the target width
|
// and expand it to fill the target width
|
||||||
@@ -2596,7 +2619,7 @@ impl Renderer {
|
|||||||
let gap_bg = if best_gap_len > 0 && best_gap_start < parsed.len() {
|
let gap_bg = if best_gap_len > 0 && best_gap_start < parsed.len() {
|
||||||
parsed[best_gap_start].2
|
parsed[best_gap_start].2
|
||||||
} else {
|
} else {
|
||||||
StatuslineColor::Rgb(0x1a, 0x1a, 0x1a)
|
default_bg_color.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// The position right before right-hand content starts (end of gap)
|
// The position right before right-hand content starts (end of gap)
|
||||||
@@ -2628,7 +2651,7 @@ impl Renderer {
|
|||||||
let fg = Self::pack_statusline_color(*fg_color);
|
let fg = Self::pack_statusline_color(*fg_color);
|
||||||
let bg = Self::pack_statusline_color(*bg_color);
|
let bg = Self::pack_statusline_color(*bg_color);
|
||||||
let style = if *bold { FontStyle::Bold } else { FontStyle::Regular };
|
let style = if *bold { FontStyle::Bold } else { FontStyle::Regular };
|
||||||
let attrs = Self::pack_attrs(*bold, false, 0, false);
|
let attrs = Self::pack_attrs(*bold, false, 0, false, false);
|
||||||
|
|
||||||
let (sprite_idx, is_colored) = if *c == ' ' || *c == '\0' {
|
let (sprite_idx, is_colored) = if *c == ' ' || *c == '\0' {
|
||||||
(0, false)
|
(0, false)
|
||||||
@@ -2677,7 +2700,7 @@ impl Renderer {
|
|||||||
let fg = Self::pack_statusline_color(fg_color);
|
let fg = Self::pack_statusline_color(fg_color);
|
||||||
let bg = Self::pack_statusline_color(bg_color);
|
let bg = Self::pack_statusline_color(bg_color);
|
||||||
let style = if bold { FontStyle::Bold } else { FontStyle::Regular };
|
let style = if bold { FontStyle::Bold } else { FontStyle::Regular };
|
||||||
let attrs = Self::pack_attrs(bold, false, 0, false);
|
let attrs = Self::pack_attrs(bold, false, 0, false, false);
|
||||||
|
|
||||||
let (sprite_idx, is_colored) = if c == ' ' || c == '\0' {
|
let (sprite_idx, is_colored) = if c == ' ' || c == '\0' {
|
||||||
(0, false)
|
(0, false)
|
||||||
@@ -2715,7 +2738,7 @@ impl Renderer {
|
|||||||
for component in section.components.iter() {
|
for component in section.components.iter() {
|
||||||
let component_fg = Self::pack_statusline_color(component.fg);
|
let component_fg = Self::pack_statusline_color(component.fg);
|
||||||
let style = if component.bold { FontStyle::Bold } else { FontStyle::Regular };
|
let style = if component.bold { FontStyle::Bold } else { FontStyle::Regular };
|
||||||
let attrs = Self::pack_attrs(component.bold, false, 0, false);
|
let attrs = Self::pack_attrs(component.bold, false, 0, false, false);
|
||||||
|
|
||||||
// Process characters with lookahead for multi-cell symbols
|
// Process characters with lookahead for multi-cell symbols
|
||||||
let chars: Vec<char> = component.text.chars().collect();
|
let chars: Vec<char> = component.text.chars().collect();
|
||||||
@@ -2844,7 +2867,7 @@ impl Renderer {
|
|||||||
|
|
||||||
// Fill remaining width with default background cells
|
// Fill remaining width with default background cells
|
||||||
// This ensures the statusline covers the entire window width
|
// This ensures the statusline covers the entire window width
|
||||||
let default_bg_packed = Self::pack_statusline_color(StatuslineColor::Default);
|
let default_bg_packed = default_bg;
|
||||||
while self.statusline_gpu_cells.len() < target_cols && self.statusline_gpu_cells.len() < self.statusline_max_cols {
|
while self.statusline_gpu_cells.len() < target_cols && self.statusline_gpu_cells.len() < self.statusline_max_cols {
|
||||||
self.statusline_gpu_cells.push(GPUCell {
|
self.statusline_gpu_cells.push(GPUCell {
|
||||||
fg: 0,
|
fg: 0,
|
||||||
@@ -2958,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.
|
||||||
@@ -3669,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);
|
||||||
@@ -3696,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 {
|
||||||
@@ -4330,10 +4352,17 @@ impl Renderer {
|
|||||||
TabBarPosition::Hidden => unreachable!(),
|
TabBarPosition::Hidden => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use same color as statusline: 0x1a1a1a (26, 26, 26) in sRGB
|
let is_light = self.palette.is_light();
|
||||||
// Pre-computed linear RGB value for srgb_to_linear(26/255) ≈ 0.00972
|
let tab_bar_bg = if is_light {
|
||||||
const TAB_BAR_BG_LINEAR: f32 = 0.00972;
|
// Light mode statusline bg is approx 0xD0, linear is ~0.63076
|
||||||
let tab_bar_bg = [TAB_BAR_BG_LINEAR, TAB_BAR_BG_LINEAR, TAB_BAR_BG_LINEAR, 1.0];
|
const TAB_BAR_BG_LINEAR_LIGHT: f32 = 0.63076;
|
||||||
|
[TAB_BAR_BG_LINEAR_LIGHT, TAB_BAR_BG_LINEAR_LIGHT, TAB_BAR_BG_LINEAR_LIGHT, 1.0]
|
||||||
|
} else {
|
||||||
|
// Use same color as statusline: 0x1a1a1a (26, 26, 26) in sRGB
|
||||||
|
// Pre-computed linear RGB value for srgb_to_linear(26/255) ≈ 0.00972
|
||||||
|
const TAB_BAR_BG_LINEAR_DARK: f32 = 0.00972;
|
||||||
|
[TAB_BAR_BG_LINEAR_DARK, TAB_BAR_BG_LINEAR_DARK, TAB_BAR_BG_LINEAR_DARK, 1.0]
|
||||||
|
};
|
||||||
|
|
||||||
// Draw tab bar background
|
// Draw tab bar background
|
||||||
log::debug!("render_panes: drawing tab bar at y={}, height={}, num_tabs={}, quads_before={}",
|
log::debug!("render_panes: drawing tab bar at y={}, height={}, num_tabs={}, quads_before={}",
|
||||||
@@ -4353,23 +4382,23 @@ impl Renderer {
|
|||||||
let tab_width = title_width.max(min_tab_width);
|
let tab_width = title_width.max(min_tab_width);
|
||||||
|
|
||||||
let tab_bg = if is_active {
|
let tab_bg = if is_active {
|
||||||
// Active tab: brightest - significantly brighter than tab bar
|
// Active tab: brightest - matches terminal background or slightly brighter
|
||||||
let [r, g, b] = self.palette.default_bg;
|
let [r, g, b] = self.palette.default_bg;
|
||||||
let boost = 50.0_f32; // More visible for active tab
|
let boost = if is_light { 0.0_f32 } else { 50.0_f32 };
|
||||||
[
|
[
|
||||||
Self::srgb_to_linear((r as f32 + boost).min(255.0) / 255.0),
|
Self::srgb_to_linear((r as f32 + boost).clamp(0.0, 255.0) / 255.0),
|
||||||
Self::srgb_to_linear((g as f32 + boost).min(255.0) / 255.0),
|
Self::srgb_to_linear((g as f32 + boost).clamp(0.0, 255.0) / 255.0),
|
||||||
Self::srgb_to_linear((b as f32 + boost).min(255.0) / 255.0),
|
Self::srgb_to_linear((b as f32 + boost).clamp(0.0, 255.0) / 255.0),
|
||||||
1.0,
|
1.0,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
// Inactive tab: slightly brighter than tab bar background
|
// Inactive tab: between tab bar background and active tab
|
||||||
let [r, g, b] = self.palette.default_bg;
|
let [r, g, b] = self.palette.default_bg;
|
||||||
let boost = 30.0_f32;
|
let boost = if is_light { -30.0_f32 } else { 30.0_f32 };
|
||||||
[
|
[
|
||||||
Self::srgb_to_linear((r as f32 + boost).min(255.0) / 255.0),
|
Self::srgb_to_linear((r as f32 + boost).clamp(0.0, 255.0) / 255.0),
|
||||||
Self::srgb_to_linear((g as f32 + boost).min(255.0) / 255.0),
|
Self::srgb_to_linear((g as f32 + boost).clamp(0.0, 255.0) / 255.0),
|
||||||
Self::srgb_to_linear((b as f32 + boost).min(255.0) / 255.0),
|
Self::srgb_to_linear((b as f32 + boost).clamp(0.0, 255.0) / 255.0),
|
||||||
1.0,
|
1.0,
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -4617,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4650,42 +4679,17 @@ impl Renderer {
|
|||||||
CursorShape::BlinkingUnderline | CursorShape::SteadyUnderline => 1,
|
CursorShape::BlinkingUnderline | CursorShape::SteadyUnderline => 1,
|
||||||
CursorShape::BlinkingBar | CursorShape::SteadyBar => 2,
|
CursorShape::BlinkingBar | CursorShape::SteadyBar => 2,
|
||||||
},
|
},
|
||||||
background_opacity: self.background_opacity,
|
background_opacity: if terminal.using_alternate_screen {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
self.background_opacity
|
||||||
|
},
|
||||||
selection_start_col: sel_start_col,
|
selection_start_col: sel_start_col,
|
||||||
selection_start_row: sel_start_row,
|
selection_start_row: sel_start_row,
|
||||||
selection_end_col: sel_end_col,
|
selection_end_col: sel_end_col,
|
||||||
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) {
|
||||||
@@ -4747,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4766,9 +4770,10 @@ impl Renderer {
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
let statusline_cols = {
|
let statusline_cols = {
|
||||||
let statusline_y = self.statusline_y();
|
let statusline_y = self.statusline_y();
|
||||||
|
let is_light = self.palette.is_light();
|
||||||
|
|
||||||
// Update statusline GPU cells from content, passing window width for gap expansion
|
// Update statusline GPU cells from content, passing window width for gap expansion
|
||||||
let cols = self.update_statusline_cells(statusline_content, width);
|
let cols = self.update_statusline_cells(statusline_content, width, is_light);
|
||||||
|
|
||||||
if cols > 0 {
|
if cols > 0 {
|
||||||
// Upload statusline cells to GPU
|
// Upload statusline cells to GPU
|
||||||
@@ -5008,10 +5013,19 @@ impl Renderer {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let [bg_r, bg_g, bg_b] = self.palette.default_bg;
|
let [bg_r, bg_g, bg_b] = self.palette.default_bg;
|
||||||
let bg_r_linear = Self::srgb_to_linear(bg_r as f32 / 255.0) as f64;
|
let mut bg_r_linear = Self::srgb_to_linear(bg_r as f32 / 255.0) as f64;
|
||||||
let bg_g_linear = Self::srgb_to_linear(bg_g as f32 / 255.0) as f64;
|
let mut bg_g_linear = Self::srgb_to_linear(bg_g as f32 / 255.0) as f64;
|
||||||
let bg_b_linear = Self::srgb_to_linear(bg_b as f32 / 255.0) as f64;
|
let mut bg_b_linear = Self::srgb_to_linear(bg_b as f32 / 255.0) as f64;
|
||||||
let bg_alpha = self.background_opacity as f64;
|
let bg_alpha = self.background_opacity as f64;
|
||||||
|
|
||||||
|
// If the compositor expects premultiplied alpha, we must premultiply the clear color.
|
||||||
|
// Otherwise, light backgrounds with opacity will look fully opaque or super-luminous.
|
||||||
|
if self.surface_config.alpha_mode == wgpu::CompositeAlphaMode::PreMultiplied {
|
||||||
|
bg_r_linear *= bg_alpha;
|
||||||
|
bg_g_linear *= bg_alpha;
|
||||||
|
bg_b_linear *= bg_alpha;
|
||||||
|
}
|
||||||
|
|
||||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("Render Pass"),
|
label: Some("Render Pass"),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
@@ -5225,20 +5239,8 @@ impl Renderer {
|
|||||||
#[cfg(feature = "render_timing")]
|
#[cfg(feature = "render_timing")]
|
||||||
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)
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
self.clear_pending_redraw();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+70
-112
@@ -38,6 +38,7 @@ pub struct Cell {
|
|||||||
pub bg_color: Color,
|
pub bg_color: Color,
|
||||||
pub bold: bool,
|
pub bold: bool,
|
||||||
pub italic: bool,
|
pub italic: bool,
|
||||||
|
pub reverse: bool,
|
||||||
/// Underline style: 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed
|
/// Underline style: 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed
|
||||||
pub underline_style: u8,
|
pub underline_style: u8,
|
||||||
/// Strikethrough decoration
|
/// Strikethrough decoration
|
||||||
@@ -45,6 +46,8 @@ pub struct Cell {
|
|||||||
/// If true, this cell is the continuation of a wide (double-width) character.
|
/// If true, this cell is the continuation of a wide (double-width) character.
|
||||||
/// The actual character is stored in the previous cell.
|
/// The actual character is stored in the previous cell.
|
||||||
pub wide_continuation: bool,
|
pub wide_continuation: bool,
|
||||||
|
/// Indicates if the line wrapped after this cell.
|
||||||
|
pub wrapped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Cell {
|
impl Default for Cell {
|
||||||
@@ -55,9 +58,11 @@ impl Default for Cell {
|
|||||||
bg_color: Color::Default,
|
bg_color: Color::Default,
|
||||||
bold: false,
|
bold: false,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
reverse: false,
|
||||||
underline_style: 0,
|
underline_style: 0,
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
wide_continuation: false,
|
wide_continuation: false,
|
||||||
|
wrapped: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,6 +186,15 @@ impl Default for ColorPalette {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ColorPalette {
|
impl ColorPalette {
|
||||||
|
/// Return whether this palette is considered "light" based on default background luminance.
|
||||||
|
pub fn is_light(&self) -> bool {
|
||||||
|
let [r, g, b] = self.default_bg;
|
||||||
|
// Standard perceived luminance calculation
|
||||||
|
let luminance =
|
||||||
|
0.299 * (r as f32) + 0.587 * (g as f32) + 0.114 * (b as f32);
|
||||||
|
luminance > 128.0
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB".
|
/// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB".
|
||||||
pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> {
|
pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> {
|
||||||
let spec = spec.trim();
|
let spec = spec.trim();
|
||||||
@@ -269,6 +283,7 @@ struct SavedCursor {
|
|||||||
strikethrough: bool,
|
strikethrough: bool,
|
||||||
origin_mode: bool,
|
origin_mode: bool,
|
||||||
auto_wrap: bool,
|
auto_wrap: bool,
|
||||||
|
reverse: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alternate screen buffer state.
|
/// Alternate screen buffer state.
|
||||||
@@ -283,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.
|
||||||
@@ -509,6 +443,8 @@ pub struct Terminal {
|
|||||||
pub current_underline_style: u8,
|
pub current_underline_style: u8,
|
||||||
/// Current strikethrough state.
|
/// Current strikethrough state.
|
||||||
pub current_strikethrough: bool,
|
pub current_strikethrough: bool,
|
||||||
|
/// Current reverse video state.
|
||||||
|
pub current_reverse: bool,
|
||||||
/// Whether the terminal content has changed.
|
/// Whether the terminal content has changed.
|
||||||
pub dirty: bool,
|
pub dirty: bool,
|
||||||
/// Bitmap of dirty lines - bit N is set if line N needs redrawing.
|
/// Bitmap of dirty lines - bit N is set if line N needs redrawing.
|
||||||
@@ -552,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>,
|
||||||
@@ -570,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();
|
||||||
|
|
||||||
@@ -594,6 +524,7 @@ impl Terminal {
|
|||||||
current_italic: false,
|
current_italic: false,
|
||||||
current_underline_style: 0,
|
current_underline_style: 0,
|
||||||
current_strikethrough: false,
|
current_strikethrough: false,
|
||||||
|
current_reverse: false,
|
||||||
dirty: true,
|
dirty: true,
|
||||||
dirty_lines: [!0u64; 4], // All lines dirty initially
|
dirty_lines: [!0u64; 4], // All lines dirty initially
|
||||||
scroll_top: 0,
|
scroll_top: 0,
|
||||||
@@ -614,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
|
||||||
@@ -650,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) {
|
||||||
@@ -697,9 +634,11 @@ impl Terminal {
|
|||||||
bg_color: self.current_bg,
|
bg_color: self.current_bg,
|
||||||
bold: self.current_bold,
|
bold: self.current_bold,
|
||||||
italic: self.current_italic,
|
italic: self.current_italic,
|
||||||
|
reverse: self.current_reverse,
|
||||||
underline_style: self.current_underline_style,
|
underline_style: self.current_underline_style,
|
||||||
strikethrough: self.current_strikethrough,
|
strikethrough: self.current_strikethrough,
|
||||||
wide_continuation,
|
wide_continuation,
|
||||||
|
wrapped: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,9 +680,11 @@ impl Terminal {
|
|||||||
bg_color: self.current_bg,
|
bg_color: self.current_bg,
|
||||||
bold: false,
|
bold: false,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
reverse: false,
|
||||||
underline_style: 0,
|
underline_style: 0,
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
wide_continuation: false,
|
wide_continuation: false,
|
||||||
|
wrapped: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,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;
|
||||||
|
|
||||||
@@ -1385,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)
|
||||||
@@ -1459,6 +1391,9 @@ impl Handler for Terminal {
|
|||||||
// Handle wrap
|
// Handle wrap
|
||||||
if self.cursor_col >= self.cols {
|
if self.cursor_col >= self.cols {
|
||||||
if self.auto_wrap {
|
if self.auto_wrap {
|
||||||
|
if self.cols > 0 {
|
||||||
|
self.grid[grid_row][0].wrapped = true;
|
||||||
|
}
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
self.advance_row();
|
self.advance_row();
|
||||||
cached_row = self.cursor_row;
|
cached_row = self.cursor_row;
|
||||||
@@ -1508,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);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1667,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),
|
||||||
);
|
);
|
||||||
@@ -1783,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;
|
||||||
@@ -1792,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' => {
|
||||||
@@ -1804,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;
|
||||||
@@ -1819,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;
|
||||||
@@ -1827,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' => {
|
||||||
@@ -1838,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' => {
|
||||||
@@ -1851,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' => {
|
||||||
@@ -1970,6 +1910,10 @@ impl Handler for Terminal {
|
|||||||
// Handle wrap
|
// Handle wrap
|
||||||
if self.cursor_col >= self.cols {
|
if self.cursor_col >= self.cols {
|
||||||
if self.auto_wrap {
|
if self.auto_wrap {
|
||||||
|
let gr = self.line_map[self.cursor_row];
|
||||||
|
if self.cols > 0 {
|
||||||
|
self.grid[gr][0].wrapped = true;
|
||||||
|
}
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
self.advance_row();
|
self.advance_row();
|
||||||
self.mark_line_dirty(self.cursor_row);
|
self.mark_line_dirty(self.cursor_row);
|
||||||
@@ -2167,15 +2111,17 @@ 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,
|
||||||
|
reverse: self.current_reverse,
|
||||||
origin_mode: self.origin_mode,
|
origin_mode: self.origin_mode,
|
||||||
auto_wrap: self.auto_wrap,
|
auto_wrap: self.auto_wrap,
|
||||||
};
|
};
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"ESC 7: Cursor saved at ({}, {}), origin_mode={}, auto_wrap={}",
|
"ESC 7: Cursor saved at ({}, {}), origin_mode={}, auto_wrap={}, reverse={}",
|
||||||
self.cursor_col,
|
self.cursor_col,
|
||||||
self.cursor_row,
|
self.cursor_row,
|
||||||
self.origin_mode,
|
self.origin_mode,
|
||||||
self.auto_wrap
|
self.auto_wrap,
|
||||||
|
self.current_reverse
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2190,14 +2136,16 @@ 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.current_reverse = self.saved_cursor.reverse;
|
||||||
self.origin_mode = self.saved_cursor.origin_mode;
|
self.origin_mode = self.saved_cursor.origin_mode;
|
||||||
self.auto_wrap = self.saved_cursor.auto_wrap;
|
self.auto_wrap = self.saved_cursor.auto_wrap;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"ESC 8: Cursor restored to ({}, {}), origin_mode={}, auto_wrap={}",
|
"ESC 8: Cursor restored to ({}, {}), origin_mode={}, auto_wrap={}, reverse={}",
|
||||||
self.cursor_col,
|
self.cursor_col,
|
||||||
self.cursor_row,
|
self.cursor_row,
|
||||||
self.origin_mode,
|
self.origin_mode,
|
||||||
self.auto_wrap
|
self.auto_wrap,
|
||||||
|
self.current_reverse
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2240,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) {
|
||||||
@@ -2249,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) {
|
||||||
@@ -2257,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) {
|
||||||
@@ -2285,9 +2236,11 @@ impl Handler for Terminal {
|
|||||||
bg_color: Color::Default,
|
bg_color: Color::Default,
|
||||||
bold: false,
|
bold: false,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
reverse: false,
|
||||||
underline_style: 0,
|
underline_style: 0,
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
wide_continuation: false,
|
wide_continuation: false,
|
||||||
|
wrapped: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.mark_line_dirty(visual_row);
|
self.mark_line_dirty(visual_row);
|
||||||
@@ -2324,6 +2277,10 @@ impl Terminal {
|
|||||||
// Check if we need to wrap before printing
|
// Check if we need to wrap before printing
|
||||||
if self.cursor_col >= self.cols {
|
if self.cursor_col >= self.cols {
|
||||||
if self.auto_wrap {
|
if self.auto_wrap {
|
||||||
|
let grid_row = self.line_map[self.cursor_row];
|
||||||
|
if self.cols > 0 {
|
||||||
|
self.grid[grid_row][0].wrapped = true;
|
||||||
|
}
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
self.advance_row();
|
self.advance_row();
|
||||||
} else {
|
} else {
|
||||||
@@ -2338,6 +2295,7 @@ impl Terminal {
|
|||||||
// Write a space in the last column and wrap
|
// Write a space in the last column and wrap
|
||||||
let grid_row = self.line_map[self.cursor_row];
|
let grid_row = self.line_map[self.cursor_row];
|
||||||
self.grid[grid_row][self.cursor_col] = Cell::default();
|
self.grid[grid_row][self.cursor_col] = Cell::default();
|
||||||
|
self.grid[grid_row][0].wrapped = true;
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
self.advance_row();
|
self.advance_row();
|
||||||
} else {
|
} else {
|
||||||
@@ -2460,15 +2418,14 @@ impl Terminal {
|
|||||||
self.current_underline_style = 1;
|
self.current_underline_style = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7 => std::mem::swap(&mut self.current_fg, &mut self.current_bg),
|
7 => self.current_reverse = true,
|
||||||
9 => self.current_strikethrough = true,
|
9 => self.current_strikethrough = true,
|
||||||
21 => self.current_underline_style = 2, // Double underline
|
21 => self.current_underline_style = 2, // Double underline
|
||||||
22 => self.current_bold = false,
|
22 => self.current_bold = false,
|
||||||
23 => self.current_italic = false,
|
23 => self.current_italic = false,
|
||||||
24 => self.current_underline_style = 0,
|
24 => self.current_underline_style = 0,
|
||||||
27 => {
|
27 => self.current_reverse = false,
|
||||||
std::mem::swap(&mut self.current_fg, &mut self.current_bg)
|
28 => self.current_reverse = false,
|
||||||
}
|
|
||||||
29 => self.current_strikethrough = false,
|
29 => self.current_strikethrough = false,
|
||||||
// Standard foreground colors (30-37)
|
// Standard foreground colors (30-37)
|
||||||
30..=37 => self.current_fg = Color::Indexed((code - 30) as u8),
|
30..=37 => self.current_fg = Color::Indexed((code - 30) as u8),
|
||||||
@@ -2517,6 +2474,7 @@ impl Terminal {
|
|||||||
self.current_italic = false;
|
self.current_italic = false;
|
||||||
self.current_underline_style = 0;
|
self.current_underline_style = 0;
|
||||||
self.current_strikethrough = false;
|
self.current_strikethrough = false;
|
||||||
|
self.current_reverse = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle Kitty keyboard protocol CSI sequences.
|
/// Handle Kitty keyboard protocol CSI sequences.
|
||||||
|
|||||||
+66
-16
@@ -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);
|
||||||
parsed_any = true;
|
if made_progress {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
*vte_state = State::Normal;
|
if *vte_state == state_before {
|
||||||
|
*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