5 Commits
21 changed files with 4705 additions and 2430 deletions
+17 -6
View File
@@ -1,6 +1,6 @@
use std::time::Instant;
use zterm::terminal::Terminal; use zterm::terminal::Terminal;
use zterm::vt_parser::Parser; use zterm::vt_parser::Parser;
use std::time::Instant;
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";
@@ -47,8 +47,14 @@ where
let mb = total_size as f64 / 1024.0 / 1024.0; let mb = total_size as f64 / 1024.0 / 1024.0;
let rate = mb / elapsed.as_secs_f64(); let rate = mb / elapsed.as_secs_f64();
println!(" {:<24} : {:>6.2}s @ {:.1} MB/s ({} reps, {:.2} MB each)", println!(
name, elapsed.as_secs_f64(), rate, repetitions, data_size as f64 / 1024.0 / 1024.0); " {:<24} : {:>6.2}s @ {:.1} MB/s ({} reps, {:.2} MB each)",
name,
elapsed.as_secs_f64(),
rate,
repetitions,
data_size as f64 / 1024.0 / 1024.0
);
} }
fn main() { fn main() {
@@ -111,7 +117,8 @@ fn main() {
} }
_ => { _ => {
// 20%: reset + cursor + repeat + mode // 20%: reset + cursor + repeat + mode
csi_data.extend_from_slice(b"\x1b[39m\x1b[10`a\x1b[100b\x1b[?1l"); csi_data
.extend_from_slice(b"\x1b[39m\x1b[10`a\x1b[100b\x1b[?1l");
} }
} }
} }
@@ -124,7 +131,9 @@ fn main() {
// Benchmark 3: Long escape codes (matches Kitty's long_escape_codes) // Benchmark 3: Long escape codes (matches Kitty's long_escape_codes)
println!("\n--- Long escape codes ---"); println!("\n--- Long escape codes ---");
let mut long_esc_data = Vec::new(); let mut long_esc_data = Vec::new();
let long_content: String = (0..8024).map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char).collect(); let long_content: String = (0..8024)
.map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char)
.collect();
for _ in 0..1024 { for _ in 0..1024 {
// OSC 6 - document reporting, ignored after parsing // OSC 6 - document reporting, ignored after parsing
long_esc_data.extend_from_slice(b"\x1b]6;"); long_esc_data.extend_from_slice(b"\x1b]6;");
@@ -137,6 +146,8 @@ fn main() {
}); });
println!("\n=== Benchmark Complete ==="); println!("\n=== Benchmark Complete ===");
println!("\nNote: These benchmarks include terminal state updates but NOT GPU rendering."); println!(
"\nNote: These benchmarks include terminal state updates but NOT GPU rendering."
);
println!("Compare with: kitten __benchmark__ (without --render flag)"); println!("Compare with: kitten __benchmark__ (without --render flag)");
} }
+158 -47
View File
@@ -16,11 +16,15 @@ use std::path::PathBuf;
/// Find a color font (emoji font) that contains the given character using fontconfig. /// Find a color font (emoji font) that contains the given character using fontconfig.
/// Returns the path to the font file if found. /// Returns the path to the font file if found.
pub fn find_color_font_for_char(c: char) -> Option<PathBuf> { pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
use fontconfig_sys as fcsys;
use fcsys::*;
use fcsys::constants::{FC_CHARSET, FC_COLOR, FC_FILE}; use fcsys::constants::{FC_CHARSET, FC_COLOR, FC_FILE};
use fcsys::*;
use fontconfig_sys as fcsys;
log::debug!("find_color_font_for_char: looking for color font for U+{:04X} '{}'", c as u32, c); log::debug!(
"find_color_font_for_char: looking for color font for U+{:04X} '{}'",
c as u32,
c
);
unsafe { unsafe {
// Create a pattern // Create a pattern
@@ -58,28 +62,54 @@ pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
let font_path = if !matched.is_null() && result == FcResultMatch { let font_path = if !matched.is_null() && result == FcResultMatch {
// Check if the matched font is actually a color font // Check if the matched font is actually a color font
let mut is_color: i32 = 0; let mut is_color: i32 = 0;
let has_color = FcPatternGetBool(matched, FC_COLOR.as_ptr() as *const i8, 0, &mut is_color) == FcResultMatch && is_color != 0; let has_color = FcPatternGetBool(
matched,
FC_COLOR.as_ptr() as *const i8,
0,
&mut is_color,
) == FcResultMatch
&& is_color != 0;
log::debug!("find_color_font_for_char: matched font, is_color={}", has_color); log::debug!(
"find_color_font_for_char: matched font, is_color={}",
has_color
);
if has_color { if has_color {
// Get the file path from the matched pattern // Get the file path from the matched pattern
let mut file_ptr: *mut u8 = std::ptr::null_mut(); let mut file_ptr: *mut u8 = std::ptr::null_mut();
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch { if FcPatternGetString(
matched,
FC_FILE.as_ptr() as *const i8,
0,
&mut file_ptr,
) == FcResultMatch
{
let path_cstr = CStr::from_ptr(file_ptr as *const i8); let path_cstr = CStr::from_ptr(file_ptr as *const i8);
let path = PathBuf::from(path_cstr.to_string_lossy().into_owned()); let path =
log::debug!("find_color_font_for_char: found color font {:?}", path); PathBuf::from(path_cstr.to_string_lossy().into_owned());
log::debug!(
"find_color_font_for_char: found color font {:?}",
path
);
Some(path) Some(path)
} else { } else {
log::debug!("find_color_font_for_char: couldn't get file path"); log::debug!(
"find_color_font_for_char: couldn't get file path"
);
None None
} }
} else { } else {
log::debug!("find_color_font_for_char: matched font is not a color font"); log::debug!(
"find_color_font_for_char: matched font is not a color font"
);
None None
} }
} else { } else {
log::debug!("find_color_font_for_char: no match found (result={:?})", result); log::debug!(
"find_color_font_for_char: no match found (result={:?})",
result
);
None None
}; };
@@ -131,11 +161,16 @@ impl ColorFontRenderer {
// Create Cairo font face from FreeType face // Create Cairo font face from FreeType face
match cairo::FontFace::create_from_ft(&ft_face) { match cairo::FontFace::create_from_ft(&ft_face) {
Ok(cairo_face) => { Ok(cairo_face) => {
self.faces.insert(path.clone(), (ft_face, cairo_face)); self.faces
.insert(path.clone(), (ft_face, cairo_face));
true true
} }
Err(e) => { Err(e) => {
log::warn!("Failed to create Cairo font face for {:?}: {:?}", path, e); log::warn!(
"Failed to create Cairo font face for {:?}: {:?}",
path,
e
);
false false
} }
} }
@@ -160,14 +195,22 @@ impl ColorFontRenderer {
cell_width: u32, cell_width: u32,
cell_height: u32, cell_height: u32,
) -> Option<(u32, u32, Vec<u8>, f32, f32)> { ) -> Option<(u32, u32, Vec<u8>, f32, f32)> {
log::debug!("render_color_glyph: U+{:04X} '{}' font={:?}", c as u32, c, font_path); log::debug!(
"render_color_glyph: U+{:04X} '{}' font={:?}",
c as u32,
c,
font_path
);
// Ensure faces are loaded // Ensure faces are loaded
if !self.ensure_faces_loaded(font_path) { if !self.ensure_faces_loaded(font_path) {
log::debug!("render_color_glyph: failed to load faces"); log::debug!("render_color_glyph: failed to load faces");
return None; return None;
} }
log::debug!("render_color_glyph: faces loaded successfully, faces count={}", self.faces.len()); log::debug!(
"render_color_glyph: faces loaded successfully, faces count={}",
self.faces.len()
);
// Get glyph index from FreeType face // Get glyph index from FreeType face
// Note: We do NOT call set_pixel_sizes here because CBDT (bitmap) fonts have fixed sizes // Note: We do NOT call set_pixel_sizes here because CBDT (bitmap) fonts have fixed sizes
@@ -175,15 +218,26 @@ impl ColorFontRenderer {
let glyph_index = { let glyph_index = {
let face_entry = self.faces.get(font_path); let face_entry = self.faces.get(font_path);
if face_entry.is_none() { if face_entry.is_none() {
log::debug!("render_color_glyph: face not found in hashmap after ensure_faces_loaded!"); log::debug!(
"render_color_glyph: face not found in hashmap after ensure_faces_loaded!"
);
return None; return None;
} }
let (ft_face, _) = face_entry?; let (ft_face, _) = face_entry?;
log::debug!("render_color_glyph: got ft_face, getting char index for U+{:04X}", c as u32); log::debug!(
"render_color_glyph: got ft_face, getting char index for U+{:04X}",
c as u32
);
let idx = ft_face.get_char_index(c as usize); let idx = ft_face.get_char_index(c as usize);
log::debug!("render_color_glyph: FreeType glyph index for U+{:04X} = {:?}", c as u32, idx); log::debug!(
"render_color_glyph: FreeType glyph index for U+{:04X} = {:?}",
c as u32,
idx
);
if idx.is_none() { if idx.is_none() {
log::debug!("render_color_glyph: glyph index is None - char not in font!"); log::debug!(
"render_color_glyph: glyph index is None - char not in font!"
);
return None; return None;
} }
idx? idx?
@@ -199,18 +253,29 @@ impl ColorFontRenderer {
let render_width = (cell_width * 2).max(cell_height) as i32; let render_width = (cell_width * 2).max(cell_height) as i32;
let render_height = cell_height as i32; let render_height = cell_height as i32;
log::debug!("render_color_glyph: render size {}x{}", render_width, render_height); log::debug!(
"render_color_glyph: render size {}x{}",
render_width,
render_height
);
// Ensure we have a large enough surface // Ensure we have a large enough surface
let surface_width = render_width.max(256); let surface_width = render_width.max(256);
let surface_height = render_height.max(256); let surface_height = render_height.max(256);
if self.surface.is_none() || self.surface_size.0 < surface_width || self.surface_size.1 < surface_height { if self.surface.is_none()
|| self.surface_size.0 < surface_width
|| self.surface_size.1 < surface_height
{
let new_width = surface_width.max(self.surface_size.0); let new_width = surface_width.max(self.surface_size.0);
let new_height = surface_height.max(self.surface_size.1); let new_height = surface_height.max(self.surface_size.1);
match ImageSurface::create(Format::ARgb32, new_width, new_height) { match ImageSurface::create(Format::ARgb32, new_width, new_height) {
Ok(surface) => { Ok(surface) => {
log::debug!("render_color_glyph: created Cairo surface {}x{}", new_width, new_height); log::debug!(
"render_color_glyph: created Cairo surface {}x{}",
new_width,
new_height
);
self.surface = Some(surface); self.surface = Some(surface);
self.surface_size = (new_width, new_height); self.surface_size = (new_width, new_height);
} }
@@ -253,8 +318,12 @@ impl ColorFontRenderer {
let mut glyph = cairo::Glyph::new(glyph_index as u64, 0.0, 0.0); let mut glyph = cairo::Glyph::new(glyph_index as u64, 0.0, 0.0);
let mut text_extents = cr.glyph_extents(&[glyph]).ok()?; let mut text_extents = cr.glyph_extents(&[glyph]).ok()?;
while current_size > min_size && (text_extents.width() > target_width || text_extents.height() > target_height) { while current_size > min_size
let ratio = (target_width / text_extents.width()).min(target_height / text_extents.height()); && (text_extents.width() > target_width
|| text_extents.height() > target_height)
{
let ratio = (target_width / text_extents.width())
.min(target_height / text_extents.height());
let new_size = (ratio * current_size).max(min_size); let new_size = (ratio * current_size).max(min_size);
if new_size >= current_size { if new_size >= current_size {
current_size -= 2.0; current_size -= 2.0;
@@ -265,24 +334,38 @@ impl ColorFontRenderer {
text_extents = cr.glyph_extents(&[glyph]).ok()?; text_extents = cr.glyph_extents(&[glyph]).ok()?;
} }
log::debug!("render_color_glyph: fitted font size {:.1} (from {:.1}), glyph extents {:.1}x{:.1}", log::debug!(
current_size, font_size_px, text_extents.width(), text_extents.height()); "render_color_glyph: fitted font size {:.1} (from {:.1}), glyph extents {:.1}x{:.1}",
current_size,
font_size_px,
text_extents.width(),
text_extents.height()
);
// Get font metrics for positioning with the final size // Get font metrics for positioning with the final size
let font_extents = cr.font_extents().ok()?; let font_extents = cr.font_extents().ok()?;
log::debug!("render_color_glyph: font extents - ascent={:.1}, descent={:.1}, height={:.1}", log::debug!(
font_extents.ascent(), font_extents.descent(), font_extents.height()); "render_color_glyph: font extents - ascent={:.1}, descent={:.1}, height={:.1}",
font_extents.ascent(),
font_extents.descent(),
font_extents.height()
);
// Create glyph with positioning at baseline // Create glyph with positioning at baseline
// y position should be at baseline (ascent from top) // y position should be at baseline (ascent from top)
glyph = cairo::Glyph::new(glyph_index as u64, 0.0, font_extents.ascent()); glyph =
cairo::Glyph::new(glyph_index as u64, 0.0, font_extents.ascent());
// Get final glyph extents for sizing // Get final glyph extents for sizing
text_extents = cr.glyph_extents(&[glyph]).ok()?; text_extents = cr.glyph_extents(&[glyph]).ok()?;
log::debug!("render_color_glyph: text extents - width={:.1}, height={:.1}, x_bearing={:.1}, y_bearing={:.1}, x_advance={:.1}", log::debug!(
text_extents.width(), text_extents.height(), "render_color_glyph: text extents - width={:.1}, height={:.1}, x_bearing={:.1}, y_bearing={:.1}, x_advance={:.1}",
text_extents.x_bearing(), text_extents.y_bearing(), text_extents.width(),
text_extents.x_advance()); text_extents.height(),
text_extents.x_bearing(),
text_extents.y_bearing(),
text_extents.x_advance()
);
// Set source color to white - the atlas stores colors directly for emoji // Set source color to white - the atlas stores colors directly for emoji
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0); cr.set_source_rgba(1.0, 1.0, 1.0, 1.0);
@@ -303,7 +386,11 @@ impl ColorFontRenderer {
let glyph_width = text_extents.width().ceil() as u32; let glyph_width = text_extents.width().ceil() as u32;
let glyph_height = text_extents.height().ceil() as u32; let glyph_height = text_extents.height().ceil() as u32;
log::debug!("render_color_glyph: glyph size {}x{}", glyph_width, glyph_height); log::debug!(
"render_color_glyph: glyph size {}x{}",
glyph_width,
glyph_height
);
if glyph_width == 0 || glyph_height == 0 { if glyph_width == 0 || glyph_height == 0 {
log::debug!("render_color_glyph: zero size glyph, returning None"); log::debug!("render_color_glyph: zero size glyph, returning None");
@@ -318,7 +405,11 @@ impl ColorFontRenderer {
let src_x = x_offset.max(0.0) as i32; let src_x = x_offset.max(0.0) as i32;
let src_y = (font_extents.ascent() + y_offset).max(0.0) as i32; let src_y = (font_extents.ascent() + y_offset).max(0.0) as i32;
log::debug!("render_color_glyph: source rect starts at ({}, {})", src_x, src_y); log::debug!(
"render_color_glyph: source rect starts at ({}, {})",
src_x,
src_y
);
// Get surface data // Get surface data
let stride = surface.stride() as usize; let stride = surface.stride() as usize;
@@ -337,10 +428,15 @@ impl ColorFontRenderer {
let src_pixel_x = src_x + x; let src_pixel_x = src_x + x;
let src_pixel_y = src_y + y; let src_pixel_y = src_y + y;
if src_pixel_x >= 0 && src_pixel_x < self.surface_size.0 if src_pixel_x >= 0
&& src_pixel_y >= 0 && src_pixel_y < self.surface_size.1 { && src_pixel_x < self.surface_size.0
let src_idx = (src_pixel_y as usize) * stride + (src_pixel_x as usize) * 4; && src_pixel_y >= 0
let dst_idx = (y as usize * out_width as usize + x as usize) * 4; && src_pixel_y < self.surface_size.1
{
let src_idx = (src_pixel_y as usize) * stride
+ (src_pixel_x as usize) * 4;
let dst_idx =
(y as usize * out_width as usize + x as usize) * 4;
if src_idx + 3 < surface_data.len() { if src_idx + 3 < surface_data.len() {
// Cairo uses ARGB in native byte order (on little-endian: BGRA in memory) // Cairo uses ARGB in native byte order (on little-endian: BGRA in memory)
@@ -361,9 +457,12 @@ impl ColorFontRenderer {
// Un-premultiply alpha if needed (Cairo uses premultiplied alpha) // Un-premultiply alpha if needed (Cairo uses premultiplied alpha)
if a > 0 && a < 255 { if a > 0 && a < 255 {
let inv_alpha = 255.0 / a as f32; let inv_alpha = 255.0 / a as f32;
rgba[dst_idx] = (r as f32 * inv_alpha).min(255.0) as u8; rgba[dst_idx] =
rgba[dst_idx + 1] = (g as f32 * inv_alpha).min(255.0) as u8; (r as f32 * inv_alpha).min(255.0) as u8;
rgba[dst_idx + 2] = (b as f32 * inv_alpha).min(255.0) as u8; rgba[dst_idx + 1] =
(g as f32 * inv_alpha).min(255.0) as u8;
rgba[dst_idx + 2] =
(b as f32 * inv_alpha).min(255.0) as u8;
rgba[dst_idx + 3] = a; rgba[dst_idx + 3] = a;
} else { } else {
rgba[dst_idx] = r; rgba[dst_idx] = r;
@@ -376,13 +475,20 @@ impl ColorFontRenderer {
} }
} }
log::debug!("render_color_glyph: extracted {}x{} pixels, {} non-zero, has_color={}", log::debug!(
out_width, out_height, non_zero_pixels, has_color); "render_color_glyph: extracted {}x{} pixels, {} non-zero, has_color={}",
out_width,
out_height,
non_zero_pixels,
has_color
);
// Check if we actually got any non-transparent pixels // Check if we actually got any non-transparent pixels
let has_content = rgba.chunks(4).any(|p| p[3] > 0); let has_content = rgba.chunks(4).any(|p| p[3] > 0);
if !has_content { if !has_content {
log::debug!("render_color_glyph: no visible content, returning None"); log::debug!(
"render_color_glyph: no visible content, returning None"
);
return None; return None;
} }
@@ -390,8 +496,13 @@ impl ColorFontRenderer {
let offset_x = text_extents.x_bearing() as f32; let offset_x = text_extents.x_bearing() as f32;
let offset_y = -text_extents.y_bearing() as f32; let offset_y = -text_extents.y_bearing() as f32;
log::debug!("render_color_glyph: SUCCESS - returning {}x{} glyph, offset=({:.1}, {:.1})", log::debug!(
out_width, out_height, offset_x, offset_y); "render_color_glyph: SUCCESS - returning {}x{} glyph, offset=({:.1}, {:.1})",
out_width,
out_height,
offset_x,
offset_y
);
Some((out_width, out_height, rgba, offset_x, offset_y)) Some((out_width, out_height, rgba, offset_x, offset_y))
} }
+11 -8
View File
@@ -8,7 +8,9 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
/// Position of the tab bar. /// Position of the tab bar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum TabBarPosition { pub enum TabBarPosition {
/// Tab bar at the top of the window. /// Tab bar at the top of the window.
@@ -81,7 +83,7 @@ impl Keybind {
"shift" => shift = true, "shift" => shift = true,
"super" | "meta" | "cmd" => super_key = true, "super" | "meta" | "cmd" => super_key = true,
"" => {} // Empty parts from splitting "" => {} // Empty parts from splitting
_ => {} // Unknown modifiers ignored _ => {} // Unknown modifiers ignored
} }
} }
@@ -283,7 +285,9 @@ impl Default for Keybindings {
impl Keybindings { impl Keybindings {
/// Builds a lookup map from parsed keybinds to actions. /// Builds a lookup map from parsed keybinds to actions.
pub fn build_action_map(&self) -> HashMap<(bool, bool, bool, bool, String), Action> { pub fn build_action_map(
&self,
) -> HashMap<(bool, bool, bool, bool, String), Action> {
let mut map = HashMap::new(); let mut map = HashMap::new();
let bindings: &[(&Keybind, Action)] = &[ let bindings: &[(&Keybind, Action)] = &[
@@ -396,9 +400,7 @@ 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) => config,
config
}
Err(e) => { Err(e) => {
log::error!("Failed to parse config file: {}", e); log::error!("Failed to parse config file: {}", e);
Self::default() Self::default()
@@ -425,8 +427,9 @@ impl Config {
fs::create_dir_all(parent)?; fs::create_dir_all(parent)?;
} }
let json = serde_json::to_string_pretty(self) let json = serde_json::to_string_pretty(self).map_err(|e| {
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
fs::write(&config_path, json)?; fs::write(&config_path, json)?;
Ok(()) Ok(())
+7 -1
View File
@@ -30,7 +30,13 @@ impl EdgeGlow {
pub const DURATION_MS: u64 = 500; pub const DURATION_MS: u64 = 500;
/// Create a new edge glow animation constrained to a pane's bounds. /// Create a new edge glow animation constrained to a pane's bounds.
pub fn new(direction: Direction, pane_x: f32, pane_y: f32, pane_width: f32, pane_height: f32) -> Self { pub fn new(
direction: Direction,
pane_x: f32,
pane_y: f32,
pane_width: f32,
pane_height: f32,
) -> Self {
Self { Self {
direction, direction,
start_time: std::time::Instant::now(), start_time: std::time::Instant::now(),
+52 -25
View File
@@ -57,8 +57,8 @@ impl FontVariant {
/// Note: For emoji, use `find_color_font_for_char` from the color_font module instead, /// Note: For emoji, use `find_color_font_for_char` from the color_font module instead,
/// which explicitly requests color fonts. /// which explicitly requests color fonts.
pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> { pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
use fontconfig_sys as fcsys;
use fcsys::*; use fcsys::*;
use fontconfig_sys as fcsys;
unsafe { unsafe {
// Create a pattern // Create a pattern
@@ -93,7 +93,12 @@ pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
// Get the file path from the matched pattern // Get the file path from the matched pattern
let mut file_ptr: *mut FcChar8 = std::ptr::null_mut(); let mut file_ptr: *mut FcChar8 = std::ptr::null_mut();
let fc_file_cstr = CStr::from_bytes_with_nul(b"file\0").unwrap(); let fc_file_cstr = CStr::from_bytes_with_nul(b"file\0").unwrap();
if FcPatternGetString(matched, fc_file_cstr.as_ptr(), 0, &mut file_ptr) == FcResultMatch if FcPatternGetString(
matched,
fc_file_cstr.as_ptr(),
0,
&mut file_ptr,
) == FcResultMatch
{ {
let path_cstr = CStr::from_ptr(file_ptr as *const i8); let path_cstr = CStr::from_ptr(file_ptr as *const i8);
Some(PathBuf::from(path_cstr.to_string_lossy().into_owned())) Some(PathBuf::from(path_cstr.to_string_lossy().into_owned()))
@@ -119,9 +124,9 @@ pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
/// Returns paths for (regular, bold, italic, bold_italic). /// Returns paths for (regular, bold, italic, bold_italic).
/// Any variant that can't be found will be None. /// Any variant that can't be found will be None.
pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] { pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
use fontconfig_sys as fcsys; use fcsys::constants::{FC_FAMILY, FC_FILE, FC_SLANT, FC_WEIGHT};
use fcsys::*; use fcsys::*;
use fcsys::constants::{FC_FAMILY, FC_WEIGHT, FC_SLANT, FC_FILE}; use fontconfig_sys as fcsys;
use std::ffi::CString; use std::ffi::CString;
let mut results: [Option<PathBuf>; 4] = [None, None, None, None]; let mut results: [Option<PathBuf>; 4] = [None, None, None, None];
@@ -149,7 +154,11 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
} }
// Set family name // Set family name
FcPatternAddString(pat, FC_FAMILY.as_ptr() as *const i8, family_cstr.as_ptr() as *const u8); FcPatternAddString(
pat,
FC_FAMILY.as_ptr() as *const i8,
family_cstr.as_ptr() as *const u8,
);
// Set weight // Set weight
FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight); FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight);
// Set slant // Set slant
@@ -163,9 +172,16 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
if result == FcResultMatch && !matched.is_null() { if result == FcResultMatch && !matched.is_null() {
let mut file_ptr: *mut u8 = std::ptr::null_mut(); let mut file_ptr: *mut u8 = std::ptr::null_mut();
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch { if FcPatternGetString(
matched,
FC_FILE.as_ptr() as *const i8,
0,
&mut file_ptr,
) == FcResultMatch
{
if !file_ptr.is_null() { if !file_ptr.is_null() {
let path_cstr = std::ffi::CStr::from_ptr(file_ptr as *const i8); let path_cstr =
std::ffi::CStr::from_ptr(file_ptr as *const i8);
if let Ok(path_str) = path_cstr.to_str() { if let Ok(path_str) = path_cstr.to_str() {
results[idx] = Some(PathBuf::from(path_str)); results[idx] = Some(PathBuf::from(path_str));
} }
@@ -210,7 +226,9 @@ pub fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
/// Load font variants for a font family. /// Load font variants for a font family.
/// Returns array of font variants, with index 0 being the regular font. /// Returns array of font variants, with index 0 being the regular font.
/// Falls back to hardcoded paths if fontconfig fails. /// Falls back to hardcoded paths if fontconfig fails.
pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'static>, [Option<FontVariant>; 4]) { pub fn load_font_family(
font_family: Option<&str>,
) -> (Box<[u8]>, FontRef<'static>, [Option<FontVariant>; 4]) {
// 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);
@@ -232,23 +250,32 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
return (font_data, primary_font, variants); return (font_data, primary_font, variants);
} }
} }
log::warn!("Failed to load font family '{}', falling back to defaults", family); log::warn!(
"Failed to load font family '{}', falling back to defaults",
family
);
} }
// Fallback: try hardcoded paths // Fallback: try hardcoded paths
let fallback_fonts = [ let fallback_fonts = [
("/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf", (
"/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf", "/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf",
"/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf", "/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf",
"/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf"), "/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf",
("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf", "/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf",
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf", ),
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Italic.ttf", (
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-BoldItalic.ttf"), "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf",
("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf", "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf",
"/usr/share/fonts/TTF/JetBrainsMono-Bold.ttf", "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Italic.ttf",
"/usr/share/fonts/TTF/JetBrainsMono-Italic.ttf", "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-BoldItalic.ttf",
"/usr/share/fonts/TTF/JetBrainsMono-BoldItalic.ttf"), ),
(
"/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf",
"/usr/share/fonts/TTF/JetBrainsMono-Bold.ttf",
"/usr/share/fonts/TTF/JetBrainsMono-Italic.ttf",
"/usr/share/fonts/TTF/JetBrainsMono-BoldItalic.ttf",
),
]; ];
for (regular, bold, italic, bold_italic) in fallback_fonts { for (regular, bold, italic, bold_italic) in fallback_fonts {
@@ -264,18 +291,18 @@ 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)),
]; ];
return (font_data, primary_font, variants); return (font_data, primary_font, variants);
} }
} }
// Last resort: try NotoSansMono // Last resort: try NotoSansMono
let noto_regular = std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf"); let noto_regular =
std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf");
if let Some(regular_variant) = load_font_variant(noto_regular) { if let Some(regular_variant) = load_font_variant(noto_regular) {
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];
return (font_data, primary_font, variants); return (font_data, primary_font, variants);
} }
+7 -1
View File
@@ -157,6 +157,7 @@ struct GridParams {
selection_start_row: i32, selection_start_row: i32,
selection_end_col: i32, selection_end_col: i32,
selection_end_row: i32, selection_end_row: i32,
selection_row_max_col: array<i32, 256>,
} }
// GPUCell instance data (matches Rust GPUCell struct) // GPUCell instance data (matches Rust GPUCell struct)
@@ -187,7 +188,7 @@ struct SpriteInfo {
var<uniform> color_table: ColorTable; var<uniform> color_table: ColorTable;
@group(1) @binding(1) @group(1) @binding(1)
var<uniform> grid_params: GridParams; var<storage, read> grid_params: GridParams;
@group(1) @binding(2) @group(1) @binding(2)
var<storage, read> cells: array<GPUCell>; var<storage, read> cells: array<GPUCell>;
@@ -278,6 +279,11 @@ fn is_cell_selected(col: u32, row: u32) -> bool {
return false; return false;
} }
// Only highlight cells that have content in them or to their right on this row
if grid_params.selection_row_max_col[row] < 0 || col > u32(grid_params.selection_row_max_col[row]) {
return false;
}
let sel_start_col = u32(grid_params.selection_start_col); let sel_start_col = u32(grid_params.selection_start_col);
let sel_start_row = u32(grid_params.selection_start_row); let sel_start_row = u32(grid_params.selection_start_row);
let sel_end_col = u32(grid_params.selection_end_col); let sel_end_col = u32(grid_params.selection_end_col);
+26 -10
View File
@@ -4,6 +4,20 @@
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility. //! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use std::sync::atomic::{AtomicU64, Ordering};
/// Unique identifier for a pane.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PaneId(pub u64);
impl PaneId {
/// Generate a new unique pane ID.
pub fn new() -> Self {
static COUNTER: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
Self(COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
}
}
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// CONSTANTS // CONSTANTS
@@ -43,19 +57,19 @@ pub const COLORED_GLYPH_FLAG: u32 = 0x80000000;
/// Pre-rendered cursor sprite indices (like Kitty's cursor_shape_map). /// Pre-rendered cursor sprite indices (like Kitty's cursor_shape_map).
/// These sprites are created at fixed indices in the sprite array after initialization. /// These sprites are created at fixed indices in the sprite array after initialization.
/// Index 0 is reserved for "no glyph" (empty cell). /// Index 0 is reserved for "no glyph" (empty cell).
pub const CURSOR_SPRITE_BEAM: u32 = 1; // Bar/beam cursor (vertical line on left) pub const CURSOR_SPRITE_BEAM: u32 = 1; // Bar/beam cursor (vertical line on left)
pub const CURSOR_SPRITE_UNDERLINE: u32 = 2; // Underline cursor (horizontal line at bottom) pub const CURSOR_SPRITE_UNDERLINE: u32 = 2; // Underline cursor (horizontal line at bottom)
pub const CURSOR_SPRITE_HOLLOW: u32 = 3; // Hollow/unfocused cursor (outline rectangle) pub const CURSOR_SPRITE_HOLLOW: u32 = 3; // Hollow/unfocused cursor (outline rectangle)
/// Pre-rendered decoration sprite indices (like Kitty's decoration sprites). /// Pre-rendered decoration sprite indices (like Kitty's decoration sprites).
/// These are created after cursor sprites and used for text decorations. /// These are created after cursor sprites and used for text decorations.
/// The shader uses these to render underlines, strikethrough, etc. /// The shader uses these to render underlines, strikethrough, etc.
pub const DECORATION_SPRITE_STRIKETHROUGH: u32 = 4; // Strikethrough line pub const DECORATION_SPRITE_STRIKETHROUGH: u32 = 4; // Strikethrough line
pub const DECORATION_SPRITE_UNDERLINE: u32 = 5; // Single underline pub const DECORATION_SPRITE_UNDERLINE: u32 = 5; // Single underline
pub const DECORATION_SPRITE_DOUBLE_UNDERLINE: u32 = 6; // Double underline pub const DECORATION_SPRITE_DOUBLE_UNDERLINE: u32 = 6; // Double underline
pub const DECORATION_SPRITE_UNDERCURL: u32 = 7; // Wavy/curly underline pub const DECORATION_SPRITE_UNDERCURL: u32 = 7; // Wavy/curly underline
pub const DECORATION_SPRITE_DOTTED: u32 = 8; // Dotted underline pub const DECORATION_SPRITE_DOTTED: u32 = 8; // Dotted underline
pub const DECORATION_SPRITE_DASHED: u32 = 9; // Dashed underline pub const DECORATION_SPRITE_DASHED: u32 = 9; // Dashed underline
/// First available sprite index for regular glyphs (after reserved cursor and decoration sprites) /// First available sprite index for regular glyphs (after reserved cursor and decoration sprites)
pub const FIRST_GLYPH_SPRITE: u32 = 10; pub const FIRST_GLYPH_SPRITE: u32 = 10;
@@ -84,7 +98,8 @@ impl GlyphVertex {
pub fn desc() -> wgpu::VertexBufferLayout<'static> { pub fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout { wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<GlyphVertex>() as wgpu::BufferAddress, array_stride: std::mem::size_of::<GlyphVertex>()
as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex, step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS, attributes: &Self::ATTRIBS,
} }
@@ -148,8 +163,8 @@ pub struct ImageUniforms {
pub src_y: f32, pub src_y: f32,
pub src_width: f32, pub src_width: f32,
pub src_height: f32, pub src_height: f32,
pub dim_factor: f32,
pub _padding1: f32, pub _padding1: f32,
pub _padding2: f32,
} }
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
@@ -225,7 +240,7 @@ pub struct FontCellMetrics {
/// works in pure NDC space without needing pixel offsets. /// works in pure NDC space without needing pixel offsets.
/// Cell dimensions are integers like Kitty for pixel-perfect rendering. /// Cell dimensions are integers like Kitty for pixel-perfect rendering.
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)] #[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct GridParams { pub struct GridParams {
pub cols: u32, pub cols: u32,
pub rows: u32, pub rows: u32,
@@ -240,6 +255,7 @@ pub struct GridParams {
pub selection_start_row: i32, pub selection_start_row: i32,
pub selection_end_col: i32, pub selection_end_col: i32,
pub selection_end_row: i32, pub selection_end_row: i32,
pub selection_row_max_col: [i32; 256],
} }
/// GPU quad instance for instanced rectangle rendering. /// GPU quad instance for instanced rectangle rendering.
+294 -90
View File
@@ -11,7 +11,7 @@ use std::time::Instant;
use base64::Engine; use base64::Engine;
use flate2::read::ZlibDecoder; use flate2::read::ZlibDecoder;
use image::{codecs::gif::GifDecoder, AnimationDecoder, ImageFormat}; use image::{AnimationDecoder, ImageFormat, codecs::gif::GifDecoder};
/// Action to perform with the graphics command. /// Action to perform with the graphics command.
#[derive(Clone, Copy, Debug, PartialEq, Default)] #[derive(Clone, Copy, Debug, PartialEq, Default)]
@@ -137,10 +137,18 @@ pub struct GraphicsCommand {
pub delete_target: DeleteTarget, pub delete_target: DeleteTarget,
/// Unicode placeholder (virtual placement). /// Unicode placeholder (virtual placement).
pub unicode_placeholder: bool, pub unicode_placeholder: bool,
/// Parent image ID (for relative placement).
pub parent_image_id: Option<u32>,
/// Parent placement ID (for relative placement).
pub parent_placement_id: Option<u32>,
/// Horizontal cell displacement from parent.
pub h_offset: i32,
/// Vertical cell displacement from parent.
pub v_offset: i32,
/// Parent image ID (for animation frames). /// Parent image ID (for animation frames).
pub parent_id: Option<u32>, pub parent_id: Option<u32>,
/// Parent placement ID (for animation frames). /// Parent placement ID (for animation frames).
pub parent_placement_id: Option<u32>, pub parent_placement_id_anim: Option<u32>,
/// Frame number (for animation). /// Frame number (for animation).
pub frame_number: Option<u32>, pub frame_number: Option<u32>,
/// Frame gap in milliseconds (z key for animation frames). /// Frame gap in milliseconds (z key for animation frames).
@@ -213,8 +221,10 @@ impl GraphicsCommand {
} }
} }
let is_animation = let is_animation = matches!(
matches!(cmd.action, Action::AnimationFrame | Action::AnimationControl); cmd.action,
Action::AnimationFrame | Action::AnimationControl
);
// Second pass: parse all keys with correct interpretation // Second pass: parse all keys with correct interpretation
for (key, value) in pairs { for (key, value) in pairs {
@@ -256,6 +266,17 @@ impl GraphicsCommand {
cmd.height = value.parse().ok(); cmd.height = value.parse().ok();
} }
} }
"P" => {
if is_animation {
// P = parent_frame_index for animation
cmd.base_frame = value.parse().ok();
} else {
cmd.parent_image_id = value.parse().ok();
}
}
"Q" => cmd.parent_placement_id = value.parse().ok(),
"H" => cmd.h_offset = value.parse().unwrap_or(0),
"V" => cmd.v_offset = value.parse().unwrap_or(0),
"x" => cmd.src_x = value.parse().unwrap_or(0), "x" => cmd.src_x = value.parse().unwrap_or(0),
"y" => cmd.src_y = value.parse().unwrap_or(0), "y" => cmd.src_y = value.parse().unwrap_or(0),
"w" => cmd.src_width = value.parse().unwrap_or(0), "w" => cmd.src_width = value.parse().unwrap_or(0),
@@ -336,7 +357,11 @@ impl GraphicsCommand {
} }
// Decode base64 payload // Decode base64 payload
log::debug!("Parsing payload: len={}, content={:?}", payload_part.len(), std::str::from_utf8(payload_part).ok()); log::debug!(
"Parsing payload: len={}, content={:?}",
payload_part.len(),
std::str::from_utf8(payload_part).ok()
);
if !payload_part.is_empty() { if !payload_part.is_empty() {
if let Ok(payload_str) = std::str::from_utf8(payload_part) { if let Ok(payload_str) = std::str::from_utf8(payload_part) {
if let Ok(decoded) = base64_decode(payload_str) { if let Ok(decoded) = base64_decode(payload_str) {
@@ -436,8 +461,13 @@ pub fn decode_gif(
return Err(GraphicsError::GifDecodeFailed); return Err(GraphicsError::GifDecodeFailed);
} }
log::debug!("Decoded GIF: {}x{}, {} frames, {}ms total duration", log::debug!(
width, height, frames.len(), total_duration_ms); "Decoded GIF: {}x{}, {} frames, {}ms total duration",
width,
height,
frames.len(),
total_duration_ms
);
let first_frame = frames[0].data.clone(); let first_frame = frames[0].data.clone();
@@ -465,7 +495,7 @@ pub fn decode_gif(
pub fn decode_webm( pub fn decode_webm(
path: &str, path: &str,
) -> Result<(u32, u32, Vec<u8>, Option<AnimationData>), GraphicsError> { ) -> Result<(u32, u32, Vec<u8>, Option<AnimationData>), GraphicsError> {
use ffmpeg::format::{input, Pixel}; use ffmpeg::format::{Pixel, input};
use ffmpeg::media::Type; use ffmpeg::media::Type;
use ffmpeg::software::scaling::{ use ffmpeg::software::scaling::{
context::Context as ScalingContext, flag::Flags, context::Context as ScalingContext, flag::Flags,
@@ -804,7 +834,9 @@ pub struct ImageStorage {
current_chunked_id: Option<u32>, current_chunked_id: Option<u32>,
/// Next auto-generated image ID. /// Next auto-generated image ID.
next_id: u32, next_id: u32,
/// Flag indicating images have changed and need re-upload to GPU. /// Images that have been updated and need re-upload to GPU.
pub dirty_images: std::collections::HashSet<u32>,
/// Flag indicating placements have changed and need re-render.
pub dirty: bool, pub dirty: bool,
} }
@@ -824,6 +856,7 @@ impl ImageStorage {
chunk_buffer: HashMap::new(), chunk_buffer: HashMap::new(),
current_chunked_id: None, current_chunked_id: None,
next_id: 1, next_id: 1,
dirty_images: std::collections::HashSet::new(),
dirty: false, dirty: false,
} }
} }
@@ -947,8 +980,15 @@ impl ImageStorage {
cell_width, cell_width,
cell_height, cell_height,
); );
log::debug!("Placed image id={} at col={} row={}, cols={} rows={}, placements={}", log::debug!(
id, cursor_col, cursor_row, cols, rows, self.placements.len()); "Placed image id={} at col={} row={}, cols={} rows={}, placements={}",
id,
cursor_col,
cursor_row,
cols,
rows,
self.placements.len()
);
Some(PlacementResult { Some(PlacementResult {
cols, cols,
rows, rows,
@@ -979,6 +1019,7 @@ impl ImageStorage {
let virtual_placement = cmd.unicode_placeholder; let virtual_placement = cmd.unicode_placeholder;
if self.images.contains_key(&id) { if self.images.contains_key(&id) {
log::debug!("Put image {}: found in storage", id);
let (cols, rows) = self.place_image( let (cols, rows) = self.place_image(
cmd, cmd,
cursor_col, cursor_col,
@@ -994,6 +1035,11 @@ impl ImageStorage {
}; };
(self.format_response(cmd, Ok(id)), Some(placement_result)) (self.format_response(cmd, Ok(id)), Some(placement_result))
} else { } else {
log::warn!(
"Put image {}: NOT found in storage! (storage size: {})",
id,
self.images.len()
);
( (
self.format_response(cmd, Err(GraphicsError::ImageNotFound)), self.format_response(cmd, Err(GraphicsError::ImageNotFound)),
None, None,
@@ -1003,37 +1049,47 @@ impl ImageStorage {
/// Handle a delete command. /// Handle a delete command.
fn handle_delete(&mut self, cmd: &GraphicsCommand) { fn handle_delete(&mut self, cmd: &GraphicsCommand) {
log::debug!(
"Delete command: target={:?}, id={:?}",
cmd.delete_target,
cmd.image_id
);
match &cmd.delete_target { match &cmd.delete_target {
DeleteTarget::All => { DeleteTarget::All => {
log::debug!("Deleting all images and placements");
self.images.clear(); self.images.clear();
self.placements.clear(); self.placements.clear();
self.dirty = true; self.dirty = true;
} }
DeleteTarget::ById(id) => { DeleteTarget::ById(id) => {
let id = cmd.image_id.unwrap_or(*id); let id = cmd.image_id.unwrap_or(*id);
self.images.remove(&id); log::debug!("Removing all placements of image by id={}", id);
self.placements.retain(|p| p.image_id != id); self.placements.retain(|p| p.image_id != id);
self.dirty = true; self.dirty = true;
} }
DeleteTarget::AtCursor => { DeleteTarget::AtCursor => {
// Would need cursor position - simplified for now log::debug!("Deleting placements at cursor");
self.placements.clear(); self.placements.clear();
self.dirty = true; self.dirty = true;
} }
_ => { _ => {
// Other delete modes not yet implemented log::debug!("Unhandled delete target: {:?}", cmd.delete_target);
} }
} }
} }
/// Handle an animation frame command (a=f). /// Handle an animation frame command (a=f).
/// This adds a frame to an existing image's animation. /// This adds a frame to an existing image's animation.
fn handle_animation_frame(&mut self, mut cmd: GraphicsCommand) -> Option<String> { fn handle_animation_frame(
&mut self,
mut cmd: GraphicsCommand,
) -> Option<String> {
let id = match cmd.image_id { let id = match cmd.image_id {
Some(id) => id, Some(id) => id,
None => { None => {
log::warn!("AnimationFrame without image_id"); log::warn!("AnimationFrame without image_id");
return self.format_response(&cmd, Err(GraphicsError::MissingId)); return self
.format_response(&cmd, Err(GraphicsError::MissingId));
} }
}; };
@@ -1056,15 +1112,25 @@ impl ImageStorage {
Ok(p) => p.trim().to_string(), Ok(p) => p.trim().to_string(),
Err(_) => { Err(_) => {
log::warn!("Invalid file path in animation frame"); log::warn!("Invalid file path in animation frame");
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed)); return self.format_response(
&cmd,
Err(GraphicsError::FileReadFailed),
);
} }
}; };
log::debug!("Reading animation frame from file: {}", path); log::debug!("Reading animation frame from file: {}", path);
match std::fs::read(&path) { match std::fs::read(&path) {
Ok(data) => cmd.payload = data, Ok(data) => cmd.payload = data,
Err(e) => { Err(e) => {
log::warn!("Failed to read animation frame file {}: {}", path, e); log::warn!(
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed)); "Failed to read animation frame file {}: {}",
path,
e
);
return self.format_response(
&cmd,
Err(GraphicsError::FileReadFailed),
);
} }
} }
// Delete temp file after reading // Delete temp file after reading
@@ -1076,20 +1142,38 @@ impl ImageStorage {
let shm_name = match std::str::from_utf8(&cmd.payload) { let shm_name = match std::str::from_utf8(&cmd.payload) {
Ok(p) => p.trim().to_string(), Ok(p) => p.trim().to_string(),
Err(_) => { Err(_) => {
log::warn!("Invalid shared memory name in animation frame"); log::warn!(
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed)); "Invalid shared memory name in animation frame"
);
return self.format_response(
&cmd,
Err(GraphicsError::FileReadFailed),
);
} }
}; };
let shm_path = format!("/dev/shm/{}", shm_name); let shm_path = format!("/dev/shm/{}", shm_name);
log::debug!("Reading animation frame from shared memory: {}", shm_path); log::debug!(
"Reading animation frame from shared memory: {}",
shm_path
);
match std::fs::read(&shm_path) { match std::fs::read(&shm_path) {
Ok(data) => { Ok(data) => {
log::debug!("Read {} bytes from shared memory", data.len()); log::debug!(
"Read {} bytes from shared memory",
data.len()
);
cmd.payload = data; cmd.payload = data;
} }
Err(e) => { Err(e) => {
log::warn!("Failed to read animation frame shm {}: {}", shm_path, e); log::warn!(
return self.format_response(&cmd, Err(GraphicsError::FileReadFailed)); "Failed to read animation frame shm {}: {}",
shm_path,
e
);
return self.format_response(
&cmd,
Err(GraphicsError::FileReadFailed),
);
} }
} }
// Remove shared memory object after reading // Remove shared memory object after reading
@@ -1125,7 +1209,10 @@ impl ImageStorage {
Format::Gif => { Format::Gif => {
// Unlikely, but handle it // Unlikely, but handle it
log::warn!("GIF format in animation frame - not supported"); log::warn!("GIF format in animation frame - not supported");
return self.format_response(&cmd, Err(GraphicsError::UnsupportedFormat)); return self.format_response(
&cmd,
Err(GraphicsError::UnsupportedFormat),
);
} }
}; };
@@ -1134,7 +1221,8 @@ impl ImageStorage {
Some(img) => img, Some(img) => img,
None => { None => {
log::warn!("AnimationFrame for non-existent image {}", id); log::warn!("AnimationFrame for non-existent image {}", id);
return self.format_response(&cmd, Err(GraphicsError::ImageNotFound)); return self
.format_response(&cmd, Err(GraphicsError::ImageNotFound));
} }
}; };
@@ -1145,7 +1233,11 @@ impl ImageStorage {
// This MUST happen before compositing so that frame 0 exists for c=1 // This MUST happen before compositing so that frame 0 exists for c=1
if image.animation.is_none() { if image.animation.is_none() {
// Debug: check base image alpha values // Debug: check base image alpha values
let transparent_count = image.data.chunks(4).filter(|p| p.len() == 4 && p[3] < 255).count(); let transparent_count = image
.data
.chunks(4)
.filter(|p| p.len() == 4 && p[3] < 255)
.count();
let total_pixels = image.data.len() / 4; let total_pixels = image.data.len() / 4;
log::debug!( log::debug!(
"Creating animation base frame: {}/{} pixels have alpha < 255, data len = {}", "Creating animation base frame: {}/{} pixels have alpha < 255, data len = {}",
@@ -1185,7 +1277,9 @@ impl ImageStorage {
if base_idx < anim.frames.len() { if base_idx < anim.frames.len() {
let base_data = &anim.frames[base_idx].data; let base_data = &anim.frames[base_idx].data;
if frame_data.len() == expected_size && base_data.len() == expected_size { if frame_data.len() == expected_size
&& base_data.len() == expected_size
{
// Both frames are full size - composite them // Both frames are full size - composite them
// composition_mode: 0 = alpha blend, 1 = overwrite // composition_mode: 0 = alpha blend, 1 = overwrite
let mut composited = base_data.clone(); let mut composited = base_data.clone();
@@ -1220,17 +1314,28 @@ impl ImageStorage {
// Standard alpha compositing: out = src + dst * (1 - src_a) // Standard alpha compositing: out = src + dst * (1 - src_a)
let inv_a = 255 - src_a32; let inv_a = 255 - src_a32;
composited[i] = ((src_r * src_a32 + dst_r * inv_a) / 255) as u8; composited[i] =
composited[i + 1] = ((src_g * src_a32 + dst_g * inv_a) / 255) as u8; ((src_r * src_a32 + dst_r * inv_a) / 255)
composited[i + 2] = ((src_b * src_a32 + dst_b * inv_a) / 255) as u8; as u8;
composited[i + 3] = (src_a32 + dst_a * inv_a / 255).min(255) as u8; composited[i + 1] =
((src_g * src_a32 + dst_g * inv_a) / 255)
as u8;
composited[i + 2] =
((src_b * src_a32 + dst_b * inv_a) / 255)
as u8;
composited[i + 3] =
(src_a32 + dst_a * inv_a / 255).min(255)
as u8;
} }
} }
// else: src_a == 0, keep base pixel (already in composited) // else: src_a == 0, keep base pixel (already in composited)
} }
// Debug: check alpha values // Debug: check alpha values
let transparent_count = composited.chunks(4).filter(|p| p.len() == 4 && p[3] < 255).count(); let transparent_count = composited
.chunks(4)
.filter(|p| p.len() == 4 && p[3] < 255)
.count();
let total_pixels = composited.len() / 4; let total_pixels = composited.len() / 4;
if transparent_count > 0 { if transparent_count > 0 {
log::debug!( log::debug!(
@@ -1241,7 +1346,9 @@ impl ImageStorage {
} }
composited composited
} else if frame_data.len() < expected_size && base_data.len() == expected_size { } else if frame_data.len() < expected_size
&& base_data.len() == expected_size
{
// Partial frame data - just use base for now // Partial frame data - just use base for now
log::debug!( log::debug!(
"Frame data size {} < expected {}, using base frame {}", "Frame data size {} < expected {}, using base frame {}",
@@ -1258,7 +1365,10 @@ impl ImageStorage {
} }
} else { } else {
// Base frame doesn't exist yet (shouldn't happen now), pad the data // Base frame doesn't exist yet (shouldn't happen now), pad the data
log::warn!("Base frame {} doesn't exist, padding data", base_frame_num); log::warn!(
"Base frame {} doesn't exist, padding data",
base_frame_num
);
let mut data = frame_data; let mut data = frame_data;
data.resize(expected_size, 0); data.resize(expected_size, 0);
data data
@@ -1321,12 +1431,16 @@ impl ImageStorage {
/// Handle an animation control command (a=a). /// Handle an animation control command (a=a).
/// This controls playback of an animated image. /// This controls playback of an animated image.
fn handle_animation_control(&mut self, cmd: &GraphicsCommand) -> Option<String> { fn handle_animation_control(
&mut self,
cmd: &GraphicsCommand,
) -> Option<String> {
let id = match cmd.image_id { let id = match cmd.image_id {
Some(id) => id, Some(id) => id,
None => { None => {
log::warn!("AnimationControl without image_id"); log::warn!("AnimationControl without image_id");
return self.format_response(cmd, Err(GraphicsError::MissingId)); return self
.format_response(cmd, Err(GraphicsError::MissingId));
} }
}; };
@@ -1342,7 +1456,8 @@ impl ImageStorage {
Some(img) => img, Some(img) => img,
None => { None => {
log::warn!("AnimationControl for non-existent image {}", id); log::warn!("AnimationControl for non-existent image {}", id);
return self.format_response(cmd, Err(GraphicsError::ImageNotFound)); return self
.format_response(cmd, Err(GraphicsError::ImageNotFound));
} }
}; };
@@ -1359,7 +1474,11 @@ impl ImageStorage {
AnimationState::Loading AnimationState::Loading
} }
3 => { 3 => {
log::debug!("Animation {} running ({} frames)", id, anim.frames.len()); log::debug!(
"Animation {} running ({} frames)",
id,
anim.frames.len()
);
// Reset frame start when starting animation // Reset frame start when starting animation
anim.frame_start = None; anim.frame_start = None;
anim.looping = true; anim.looping = true;
@@ -1375,7 +1494,11 @@ impl ImageStorage {
anim.current_frame = frame_num as usize - 1; // 1-indexed to 0-indexed anim.current_frame = frame_num as usize - 1; // 1-indexed to 0-indexed
// No need to clone - renderer uses current_frame_data() // No need to clone - renderer uses current_frame_data()
anim.frame_start = None; // Reset timing anim.frame_start = None; // Reset timing
log::debug!("Animation {} jumped to frame {}", id, frame_num); log::debug!(
"Animation {} jumped to frame {}",
id,
frame_num
);
} }
} }
@@ -1388,7 +1511,11 @@ impl ImageStorage {
anim.looping = true; anim.looping = true;
anim.loops_remaining = Some(loop_count); anim.loops_remaining = Some(loop_count);
} }
log::debug!("Animation {} loop count set to {:?}", id, anim.loops_remaining); log::debug!(
"Animation {} loop count set to {:?}",
id,
anim.loops_remaining
);
} }
self.dirty = true; self.dirty = true;
@@ -1451,7 +1578,9 @@ impl ImageStorage {
} }
// Delete temp file after reading // Delete temp file after reading
if cmd.transmission == Transmission::TempFile && file_path.is_none() { if cmd.transmission == Transmission::TempFile
&& file_path.is_none()
{
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
} }
} }
@@ -1485,7 +1614,9 @@ impl ImageStorage {
// Payload is already the data // Payload is already the data
// Try to detect format from magic bytes if format is default // Try to detect format from magic bytes if format is default
if cmd.format == Format::Rgba && cmd.payload.len() >= 6 { if cmd.format == Format::Rgba && cmd.payload.len() >= 6 {
if &cmd.payload[0..6] == b"GIF89a" || &cmd.payload[0..6] == b"GIF87a" { if &cmd.payload[0..6] == b"GIF89a"
|| &cmd.payload[0..6] == b"GIF87a"
{
cmd.format = Format::Gif; cmd.format = Format::Gif;
} }
} }
@@ -1516,26 +1647,36 @@ impl ImageStorage {
(w, h, d, None) (w, h, d, None)
} }
Format::Rgba => { Format::Rgba => {
let w = cmd.width.ok_or(GraphicsError::MissingDimensions)?; let w =
let h = cmd.height.ok_or(GraphicsError::MissingDimensions)?; cmd.width.ok_or(GraphicsError::MissingDimensions)?;
let h =
cmd.height.ok_or(GraphicsError::MissingDimensions)?;
let expected_size = (w * h * 4) as usize; let expected_size = (w * h * 4) as usize;
if cmd.payload.len() != expected_size { if cmd.payload.len() != expected_size {
log::warn!( log::warn!(
"RGBA image size mismatch: declared {}x{} ({} bytes expected), got {} bytes", "RGBA image size mismatch: declared {}x{} ({} bytes expected), got {} bytes",
w, h, expected_size, cmd.payload.len() w,
h,
expected_size,
cmd.payload.len()
); );
return Err(GraphicsError::InvalidData); return Err(GraphicsError::InvalidData);
} }
(w, h, cmd.payload.clone(), None) (w, h, cmd.payload.clone(), None)
} }
Format::Rgb => { Format::Rgb => {
let w = cmd.width.ok_or(GraphicsError::MissingDimensions)?; let w =
let h = cmd.height.ok_or(GraphicsError::MissingDimensions)?; cmd.width.ok_or(GraphicsError::MissingDimensions)?;
let h =
cmd.height.ok_or(GraphicsError::MissingDimensions)?;
let expected_size = (w * h * 3) as usize; let expected_size = (w * h * 3) as usize;
if cmd.payload.len() != expected_size { if cmd.payload.len() != expected_size {
log::warn!( log::warn!(
"RGB image size mismatch: declared {}x{} ({} bytes expected), got {} bytes", "RGB image size mismatch: declared {}x{} ({} bytes expected), got {} bytes",
w, h, expected_size, cmd.payload.len() w,
h,
expected_size,
cmd.payload.len()
); );
return Err(GraphicsError::InvalidData); return Err(GraphicsError::InvalidData);
} }
@@ -1562,6 +1703,7 @@ impl ImageStorage {
animation, animation,
}, },
); );
self.dirty_images.insert(id);
self.dirty = true; self.dirty = true;
Ok(id) Ok(id)
@@ -1611,6 +1753,24 @@ impl ImageStorage {
cmd.rows as usize cmd.rows as usize
}; };
// Handle relative positioning
let (final_col, final_row) = if let Some(p_id) = cmd.parent_image_id {
let q_id = cmd.parent_placement_id.unwrap_or(0);
if let Some(parent) = self
.placements
.iter()
.find(|p| p.image_id == p_id && p.placement_id == q_id)
{
let col = parent.col as i32 + cmd.h_offset;
let row = parent.row as i32 + cmd.v_offset;
(col.max(0) as usize, row.max(0) as usize)
} else {
(cursor_col, cursor_row)
}
} else {
(cursor_col, cursor_row)
};
// Don't create actual placement for virtual placements (U=1) // Don't create actual placement for virtual placements (U=1)
// Virtual placements are referenced by Unicode placeholders // Virtual placements are referenced by Unicode placeholders
if cmd.unicode_placeholder { if cmd.unicode_placeholder {
@@ -1626,8 +1786,8 @@ impl ImageStorage {
let placement = ImagePlacement { let placement = ImagePlacement {
image_id: id, image_id: id,
placement_id: cmd.placement_id.unwrap_or(0), placement_id: cmd.placement_id.unwrap_or(0),
col: cursor_col, col: final_col,
row: cursor_row, row: final_row,
cols, cols,
rows, rows,
z_index: cmd.z_index, z_index: cmd.z_index,
@@ -1639,12 +1799,9 @@ impl ImageStorage {
y_offset: cmd.y_offset, y_offset: cmd.y_offset,
}; };
// Remove existing placement with same ID if present let pid = cmd.placement_id.unwrap_or(0);
if cmd.placement_id.is_some() { self.placements
self.placements.retain(|p| { .retain(|p| p.image_id != id || p.placement_id != pid);
p.image_id != id || p.placement_id != placement.placement_id
});
}
self.placements.push(placement); self.placements.push(placement);
self.dirty = true; self.dirty = true;
@@ -1716,6 +1873,24 @@ impl ImageStorage {
&self.placements &self.placements
} }
/// Shift all image placements by a delta (e.g., when scrollback buffer wraps).
pub fn shift_placements(&mut self, delta: isize) {
if delta == 0 {
return;
}
self.placements.retain_mut(|p| {
let new_row = p.row as isize + delta;
if new_row < 0 {
false
} else {
p.row = new_row as usize;
true
}
});
self.dirty = true;
}
/// Get an image by ID. /// Get an image by ID.
pub fn get_image(&self, id: u32) -> Option<&ImageData> { pub fn get_image(&self, id: u32) -> Option<&ImageData> {
self.images.get(&id) self.images.get(&id)
@@ -1724,6 +1899,7 @@ impl ImageStorage {
/// Clear the dirty flag. /// Clear the dirty flag.
pub fn clear_dirty(&mut self) { pub fn clear_dirty(&mut self) {
self.dirty = false; self.dirty = false;
self.dirty_images.clear();
} }
/// Update animations and return list of image IDs that changed frames. /// Update animations and return list of image IDs that changed frames.
@@ -1742,13 +1918,19 @@ impl ImageStorage {
// Initialize frame start time if not set // Initialize frame start time if not set
if anim.frame_start.is_none() { if anim.frame_start.is_none() {
anim.frame_start = Some(now); anim.frame_start = Some(now);
log::debug!("Animation {} started, {} frames, first frame {}ms", log::debug!(
id, anim.frames.len(), anim.frames[0].duration_ms); "Animation {} started, {} frames, first frame {}ms",
id,
anim.frames.len(),
anim.frames[0].duration_ms
);
} }
let frame_start = anim.frame_start.unwrap(); let frame_start = anim.frame_start.unwrap();
let elapsed = now.duration_since(frame_start).as_millis() as u32; let elapsed =
let current_frame_duration = anim.frames[anim.current_frame].duration_ms; now.duration_since(frame_start).as_millis() as u32;
let current_frame_duration =
anim.frames[anim.current_frame].duration_ms;
if elapsed >= current_frame_duration { if elapsed >= current_frame_duration {
// Advance to next frame // Advance to next frame
@@ -1758,36 +1940,56 @@ impl ImageStorage {
if anim.looping { if anim.looping {
// Check loop count // Check loop count
if let Some(ref mut loops) = anim.loops_remaining { if let Some(ref mut loops) = anim.loops_remaining {
if *loops > 0 { if *loops > 0 {
log::debug!("Animation {} looping, {} loops remaining", id, *loops - 1); log::debug!(
*loops -= 1; "Animation {} looping, {} loops remaining",
anim.current_frame = 0; id,
} else { *loops - 1
log::debug!("Animation {} stopped: no more loops", id); );
// No more loops, stop *loops -= 1;
anim.state = AnimationState::Stopped; anim.current_frame = 0;
continue; } else {
} log::debug!(
"Animation {} stopped: no more loops",
} else { id
log::debug!("Animation {} looping indefinitely", id); );
// Infinite looping // No more loops, stop
anim.current_frame = 0; anim.state = AnimationState::Stopped;
} continue;
}
} } else {
log::debug!("Animation {} reached end, looping={}", id, anim.looping); log::debug!(
if !anim.looping { "Animation {} looping indefinitely",
log::debug!("Animation {} stopping (looping=false)", id); id
} );
// else: stay on last frame // Infinite looping
} else { anim.current_frame = 0;
}
}
log::debug!(
"Animation {} reached end, looping={}",
id,
anim.looping
);
if !anim.looping {
log::debug!(
"Animation {} stopping (looping=false)",
id
);
}
// else: stay on last frame
} else {
anim.current_frame = next_frame; anim.current_frame = next_frame;
} }
log::debug!("Animation {} frame {} -> {} (elapsed {}ms >= {}ms)", log::debug!(
id, old_frame, anim.current_frame, elapsed, current_frame_duration); "Animation {} frame {} -> {} (elapsed {}ms >= {}ms)",
id,
old_frame,
anim.current_frame,
elapsed,
current_frame_duration
);
// Just update frame index - no data clone needed! // Just update frame index - no data clone needed!
// The renderer will use current_frame_data() to get the right frame. // The renderer will use current_frame_data() to get the right frame.
@@ -1809,7 +2011,9 @@ impl ImageStorage {
self.images.values().any(|img| { self.images.values().any(|img| {
img.animation img.animation
.as_ref() .as_ref()
.map(|a| a.state == AnimationState::Running && a.frames.len() > 1) .map(|a| {
a.state == AnimationState::Running && a.frames.len() > 1
})
.unwrap_or(false) .unwrap_or(false)
}) })
} }
+234 -93
View File
@@ -3,9 +3,9 @@
//! This module handles GPU-accelerated rendering of images in the terminal, //! This module handles GPU-accelerated rendering of images in the terminal,
//! supporting the Kitty Graphics Protocol for inline image display. //! supporting the Kitty Graphics Protocol for inline image display.
use std::collections::HashMap; use crate::gpu_types::{ImageUniforms, PaneId};
use crate::gpu_types::ImageUniforms;
use crate::graphics::{ImageData, ImagePlacement, ImageStorage}; use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
use std::collections::HashMap;
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// GPU IMAGE // GPU IMAGE
@@ -15,7 +15,6 @@ use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
pub struct GpuImage { pub struct GpuImage {
pub texture: wgpu::Texture, pub texture: wgpu::Texture,
pub view: wgpu::TextureView, pub view: wgpu::TextureView,
pub uniform_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup, pub bind_group: wgpu::BindGroup,
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
@@ -28,12 +27,20 @@ pub struct GpuImage {
/// Manages GPU resources for image rendering. /// Manages GPU resources for image rendering.
/// Handles uploading, caching, and preparing images for rendering. /// Handles uploading, caching, and preparing images for rendering.
pub struct ImageRenderer { pub struct ImageRenderer {
/// Bind group layout for image rendering. /// Bind group layout for uniforms.
bind_group_layout: wgpu::BindGroupLayout, uniform_layout: wgpu::BindGroupLayout,
/// Bind group layout for textures.
texture_layout: wgpu::BindGroupLayout,
/// Sampler for image textures. /// Sampler for image textures.
sampler: wgpu::Sampler, sampler: wgpu::Sampler,
/// Cached GPU textures for images, keyed by image ID. /// Cached GPU textures for images, keyed by (pane_id, image_id).
textures: HashMap<u32, GpuImage>, textures: HashMap<(PaneId, u32), GpuImage>,
/// Global uniform buffer for image renders.
pub uniform_buffer: wgpu::Buffer,
/// Bind group for image uniforms.
uniform_bind_group: wgpu::BindGroup,
/// Minimum offset alignment for uniform buffers.
pub alignment: u64,
} }
impl ImageRenderer { impl ImageRenderer {
@@ -51,65 +58,137 @@ impl ImageRenderer {
..Default::default() ..Default::default()
}); });
// Create bind group layout for images // Create bind group layout for uniforms (binding 0)
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { let uniform_layout =
label: Some("Image Bind Group Layout"), device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[ label: Some("Image Uniform Layout"),
wgpu::BindGroupLayoutEntry { entries: &[wgpu::BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, visibility: wgpu::ShaderStages::VERTEX
| wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform, ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false, has_dynamic_offset: true,
min_binding_size: None, min_binding_size: None,
}, },
count: None, count: None,
}, }],
wgpu::BindGroupLayoutEntry { });
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT, // Create bind group layout for textures (binding 1, 2)
ty: wgpu::BindingType::Texture { let texture_layout =
sample_type: wgpu::TextureSampleType::Float { filterable: true }, device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
view_dimension: wgpu::TextureViewDimension::D2, label: Some("Image Texture Layout"),
multisampled: false, entries: &[
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}, },
count: None, wgpu::BindGroupLayoutEntry {
}, binding: 2,
wgpu::BindGroupLayoutEntry { visibility: wgpu::ShaderStages::FRAGMENT,
binding: 2, ty: wgpu::BindingType::Sampler(
visibility: wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), ),
count: None, count: None,
}, },
], ],
});
// Create a large uniform buffer for all image renders in a frame
// Max 256 images per frame (65536 / 256)
let buffer_size = 65536;
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Uniform Buffer"),
size: buffer_size,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}); });
// Create the uniform bind group
let uniform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Image Uniform Bind Group"),
layout: &uniform_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: &uniform_buffer,
offset: 0,
size: std::num::NonZeroU64::new(
std::mem::size_of::<ImageUniforms>() as u64,
),
},
),
}],
});
let alignment =
device.limits().min_uniform_buffer_offset_alignment as u64;
Self { Self {
bind_group_layout, uniform_layout,
texture_layout,
sampler, sampler,
textures: HashMap::new(), textures: HashMap::new(),
uniform_buffer,
uniform_bind_group,
alignment,
} }
} }
/// Get the bind group layout for creating the image pipeline. /// Get the uniform bind group layout.
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout { pub fn uniform_layout(&self) -> &wgpu::BindGroupLayout {
&self.bind_group_layout &self.uniform_layout
}
/// Get the texture bind group layout.
pub fn texture_layout(&self) -> &wgpu::BindGroupLayout {
&self.texture_layout
}
/// Get the uniform bind group.
pub fn uniform_bind_group(&self) -> &wgpu::BindGroup {
&self.uniform_bind_group
} }
/// Get a GPU image by ID. /// Get a GPU image by ID.
pub fn get(&self, image_id: &u32) -> Option<&GpuImage> { pub fn get(&self, pane_id: PaneId, image_id: &u32) -> Option<&GpuImage> {
self.textures.get(image_id) self.textures.get(&(pane_id, *image_id))
} }
/// Upload an image to the GPU, creating or updating its texture. /// Upload an image to the GPU, creating or updating its texture.
pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, image: &ImageData) { pub fn upload_image(
log::debug!("upload_image: id={}, width={}, height={}, data_len={}", image.id, image.width, image.height, image.data.len()); &mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pane_id: PaneId,
image: &ImageData,
) {
log::debug!(
"upload_image: pane_id={:?}, id={}, width={}, height={}, data_len={}",
pane_id,
image.id,
image.width,
image.height,
image.data.len()
);
// Get current frame data (handles animation frames automatically) // Get current frame data (handles animation frames automatically)
let data = image.current_frame_data(); let data = image.current_frame_data();
// Check if we already have this image // Check if we already have this image
if let Some(existing) = self.textures.get(&image.id) { if let Some(existing) = self.textures.get(&(pane_id, image.id)) {
if existing.width == image.width && existing.height == image.height { if existing.width == image.width && existing.height == image.height
{
// Same dimensions, just update the data // Same dimensions, just update the data
queue.write_texture( queue.write_texture(
wgpu::TexelCopyTextureInfo { wgpu::TexelCopyTextureInfo {
@@ -137,7 +216,7 @@ impl ImageRenderer {
// Create new texture // Create new texture
let texture = device.create_texture(&wgpu::TextureDescriptor { let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Image {}", image.id)), label: Some(&format!("Image {} (pane {:?})", image.id, pane_id)),
size: wgpu::Extent3d { size: wgpu::Extent3d {
width: image.width, width: image.width,
height: image.height, height: image.height,
@@ -147,7 +226,8 @@ impl ImageRenderer {
sample_count: 1, sample_count: 1,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST,
view_formats: &[], view_formats: &[],
}); });
@@ -174,23 +254,13 @@ impl ImageRenderer {
let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
// Create per-image uniform buffer
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(&format!("Image {} Uniform Buffer", image.id)),
size: std::mem::size_of::<ImageUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Create bind group for this image with its own uniform buffer
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("Image {} Bind Group", image.id)), label: Some(&format!(
layout: &self.bind_group_layout, "Image {} (pane {:?}) Bind Group",
image.id, pane_id
)),
layout: &self.texture_layout,
entries: &[ entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 1, binding: 1,
resource: wgpu::BindingResource::TextureView(&view), resource: wgpu::BindingResource::TextureView(&view),
@@ -202,14 +272,16 @@ impl ImageRenderer {
], ],
}); });
self.textures.insert(image.id, GpuImage { self.textures.insert(
texture, (pane_id, image.id),
view, GpuImage {
uniform_buffer, texture,
bind_group, view,
width: image.width, bind_group,
height: image.height, width: image.width,
}); height: image.height,
},
);
log::debug!( log::debug!(
"Uploaded image {} ({}x{}) to GPU", "Uploaded image {} ({}x{}) to GPU",
@@ -220,52 +292,89 @@ impl ImageRenderer {
} }
/// Remove an image from the GPU. /// Remove an image from the GPU.
pub fn remove_image(&mut self, image_id: u32) { pub fn remove_image(&mut self, pane_id: PaneId, image_id: u32) {
if self.textures.remove(&image_id).is_some() { if self.textures.remove(&(pane_id, image_id)).is_some() {
log::debug!("Removed image {} from GPU", image_id); log::debug!(
"Removed image {} (pane {:?}) from GPU",
image_id,
pane_id
);
} }
} }
/// Sync images from terminal's image storage to GPU. /// Sync images from terminal's image storage to GPU.
/// Uploads new/changed images and removes deleted ones. /// Uploads new/changed images and removes deleted ones.
/// Also updates animation frames. /// Also updates animation frames.
pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) { pub fn sync_images(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pane_id: PaneId,
storage: &mut ImageStorage,
) {
// Update animations and get list of changed image IDs // Update animations and get list of changed image IDs
let changed_ids = storage.update_animations(); let changed_ids = storage.update_animations();
log::debug!("Sync images: changed_ids={:?}, dirty={}", changed_ids, storage.dirty); log::debug!(
"Sync images: pane_id={:?}, changed_ids={:?}, dirty={}",
pane_id,
changed_ids,
storage.dirty
);
// Re-upload frames that changed due to animation // Re-upload frames that changed due to animation
for id in &changed_ids { for id in &changed_ids {
if let Some(image) = storage.get_image(*id) { if let Some(image) = storage.get_image(*id) {
self.upload_image(device, queue, image); self.upload_image(device, queue, pane_id, image);
} }
} }
if !storage.dirty && changed_ids.is_empty() { if !storage.dirty && changed_ids.is_empty() {
log::debug!(
"Sync images: skipping upload (not dirty, no animations)"
);
return; return;
} }
// Upload all images (upload_image handles deduplication) // Upload images that were marked as dirty (newly transmitted or modified)
for image in storage.images().values() { for id in &storage.dirty_images {
self.upload_image(device, queue, image); log::debug!("Sync images: uploading dirty image id={:?}", id);
} if let Some(image) = storage.get_image(*id) {
self.upload_image(device, queue, pane_id, image);
// Remove textures for deleted images
let current_ids: std::collections::HashSet<u32> = storage.images().keys().copied().collect();
let gpu_ids: Vec<u32> = self.textures.keys().copied().collect();
for id in gpu_ids {
if !current_ids.contains(&id) {
self.remove_image(id);
} }
} }
storage.clear_dirty(); storage.clear_dirty();
} }
/// Remove images from the GPU that are not present in the provided set of active images.
/// active_images is a set of (pane_id, image_id) tuples.
pub fn gc_images(
&mut self,
active_images: &std::collections::HashSet<(PaneId, u32)>,
) {
let gpu_ids: Vec<(PaneId, u32)> =
self.textures.keys().copied().collect();
let mut removed_count = 0;
for id in gpu_ids {
if !active_images.contains(&id) {
log::debug!(
"GC: removing image {:?} as it is no longer active",
id
);
self.remove_image(id.0, id.1);
removed_count += 1;
}
}
if removed_count > 0 {
log::debug!("GC images: removed {} unused textures", removed_count);
}
}
/// Prepare image renders for a pane. /// Prepare image renders for a pane.
/// Returns a Vec of (image_id, uniforms) for deferred rendering. /// Returns a Vec of (image_id, uniforms) for deferred rendering.
pub fn prepare_image_renders( pub fn prepare_image_renders(
&self, &self,
pane_id: PaneId,
placements: &[ImagePlacement], placements: &[ImagePlacement],
pane_x: f32, pane_x: f32,
pane_y: f32, pane_y: f32,
@@ -276,37 +385,69 @@ impl ImageRenderer {
scrollback_len: usize, scrollback_len: usize,
scroll_offset: usize, scroll_offset: usize,
visible_rows: usize, visible_rows: usize,
dim_factor: f32,
) -> Vec<(u32, ImageUniforms)> { ) -> Vec<(u32, ImageUniforms)> {
log::debug!(
"prepare_image_renders: pane={:?}, placements={}, scrollback={}, offset={}, rows={}",
pane_id,
placements.len(),
scrollback_len,
scroll_offset,
visible_rows
);
let mut renders = Vec::new(); let mut renders = Vec::new();
for placement in placements { for placement in placements {
// Check if we have the GPU texture for this image // Check if we have the GPU texture for this image
let gpu_image = match self.textures.get(&placement.image_id) { let gpu_image =
Some(img) => img, match self.textures.get(&(pane_id, placement.image_id)) {
None => continue, // Skip if not uploaded yet Some(img) => img,
}; None => {
log::debug!(
"Image {} not found in GPU cache for pane {:?}",
placement.image_id,
pane_id
);
continue;
}
};
// Convert absolute row to visible screen row // Convert absolute row to visible screen row
// placement.row is absolute (scrollback_len_at_placement + cursor_row) // placement.row is absolute (scrollback_len_at_placement + cursor_row)
// visible_row = absolute_row - scrollback_len + scroll_offset // visible_row = absolute_row - scrollback_len + scroll_offset
let absolute_row = placement.row as isize; let absolute_row = placement.row as isize;
let visible_row = absolute_row - scrollback_len as isize + scroll_offset as isize; let visible_row =
absolute_row - scrollback_len as isize + scroll_offset as isize;
// Check if image is visible on screen // Check if image is visible on screen
// Image spans from visible_row to visible_row + placement.rows // Image spans from visible_row to visible_row + placement.rows
let image_bottom = visible_row + placement.rows as isize; let image_bottom = visible_row + placement.rows as isize;
if image_bottom < 0 || visible_row >= visible_rows as isize { if image_bottom < 0 || visible_row >= visible_rows as isize {
log::debug!("Image {} culled: visible_row={}, image_bottom={}, visible_rows={}", placement.image_id, visible_row, image_bottom, visible_rows); log::debug!(
"Image {} culled: visible_row={}, image_bottom={}, visible_rows={}",
placement.image_id,
visible_row,
image_bottom,
visible_rows
);
continue; // Image is completely off-screen continue; // Image is completely off-screen
} }
// Calculate display position in pixels // Calculate display position in pixels
let pos_x = pane_x + (placement.col as f32 * cell_width) + placement.x_offset as f32; let pos_x = pane_x
let pos_y = pane_y + (visible_row as f32 * cell_height) + placement.y_offset as f32; + (placement.col as f32 * cell_width)
+ placement.x_offset as f32;
let pos_y = pane_y
+ (visible_row as f32 * cell_height)
+ placement.y_offset as f32;
log::debug!( log::debug!(
"Image render: pane_x={} col={} cell_width={} x_offset={} => pos_x={}", "Image render: pane_x={} col={} cell_width={} x_offset={} => pos_x={}",
pane_x, placement.col, cell_width, placement.x_offset, pos_x pane_x,
placement.col,
cell_width,
placement.x_offset,
pos_x
); );
// Calculate display size in pixels // Calculate display size in pixels
@@ -338,8 +479,8 @@ impl ImageRenderer {
src_y, src_y,
src_width, src_width,
src_height, src_height,
dim_factor,
_padding1: 0.0, _padding1: 0.0,
_padding2: 0.0,
}; };
renders.push((placement.image_id, uniforms)); renders.push((placement.image_id, uniforms));
+8 -4
View File
@@ -16,18 +16,19 @@ struct ImageUniforms {
src_y: f32, src_y: f32,
src_width: f32, src_width: f32,
src_height: f32, src_height: f32,
// Dim factor for unfocused panes (1.0 = bright, 0.0 = dimmed)
dim_factor: f32,
// Padding for alignment // Padding for alignment
_padding1: f32, _padding1: f32,
_padding2: f32,
} }
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> uniforms: ImageUniforms; var<uniform> uniforms: ImageUniforms;
@group(0) @binding(1) @group(1) @binding(1)
var image_texture: texture_2d<f32>; var image_texture: texture_2d<f32>;
@group(0) @binding(2) @group(1) @binding(2)
var image_sampler: sampler; var image_sampler: sampler;
struct VertexOutput { struct VertexOutput {
@@ -87,6 +88,9 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Sample the image texture // Sample the image texture
let color = textureSample(image_texture, image_sampler, in.uv); let color = textureSample(image_texture, image_sampler, in.uv);
// Apply dimming factor to RGB channels
let dimmed_rgb = color.rgb * uniforms.dim_factor;
// Return with premultiplied alpha for proper blending // Return with premultiplied alpha for proper blending
return vec4<f32>(color.rgb * color.a, color.a); return vec4<f32>(dimmed_rgb * color.a, color.a);
} }
+1 -5
View File
@@ -73,11 +73,7 @@ impl Modifiers {
bits |= 128; bits |= 128;
} }
if bits == 0 { if bits == 0 { None } else { Some(1 + bits) }
None
} else {
Some(1 + bits)
}
} }
/// Returns true if any modifier is active. /// Returns true if any modifier is active.
+2 -2
View File
@@ -6,8 +6,8 @@ pub mod box_drawing;
pub mod color; pub mod color;
pub mod color_font; pub mod color_font;
pub mod config; pub mod config;
pub mod font_loader;
pub mod edge_glow; pub mod edge_glow;
pub mod font_loader;
pub mod gpu_types; pub mod gpu_types;
pub mod graphics; pub mod graphics;
pub mod image_renderer; pub mod image_renderer;
@@ -16,8 +16,8 @@ pub mod pane_resources;
pub mod pipeline; pub mod pipeline;
pub mod pty; pub mod pty;
pub mod renderer; pub mod renderer;
pub mod simd_utf8;
pub mod statusline; pub mod statusline;
pub mod terminal; pub mod terminal;
pub mod simd_utf8;
pub mod vt_parser; pub mod vt_parser;
mod vt_test_osc; mod vt_test_osc;
+484 -240
View File
File diff suppressed because it is too large Load Diff
+57 -36
View File
@@ -24,12 +24,30 @@ impl<'a> PipelineBuilder<'a> {
layout: &'a wgpu::PipelineLayout, layout: &'a wgpu::PipelineLayout,
format: wgpu::TextureFormat, format: wgpu::TextureFormat,
) -> Self { ) -> Self {
Self { device, shader, layout, format } Self {
device,
shader,
layout,
format,
}
} }
/// Build a pipeline with TriangleStrip topology and no vertex buffers (most common case). /// Build a pipeline with TriangleStrip topology and no vertex buffers (most common case).
pub fn build(&self, label: &str, vs_entry: &str, fs_entry: &str, blend: wgpu::BlendState) -> wgpu::RenderPipeline { pub fn build(
self.build_full(label, vs_entry, fs_entry, blend, wgpu::PrimitiveTopology::TriangleStrip, &[]) &self,
label: &str,
vs_entry: &str,
fs_entry: &str,
blend: wgpu::BlendState,
) -> wgpu::RenderPipeline {
self.build_full(
label,
vs_entry,
fs_entry,
blend,
wgpu::PrimitiveTopology::TriangleStrip,
&[],
)
} }
/// Build a pipeline with custom topology and vertex buffers. /// Build a pipeline with custom topology and vertex buffers.
@@ -42,38 +60,41 @@ impl<'a> PipelineBuilder<'a> {
topology: wgpu::PrimitiveTopology, topology: wgpu::PrimitiveTopology,
vertex_buffers: &[wgpu::VertexBufferLayout<'_>], vertex_buffers: &[wgpu::VertexBufferLayout<'_>],
) -> wgpu::RenderPipeline { ) -> wgpu::RenderPipeline {
self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { self.device
label: Some(label), .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
layout: Some(self.layout), label: Some(label),
vertex: wgpu::VertexState { layout: Some(self.layout),
module: self.shader, vertex: wgpu::VertexState {
entry_point: Some(vs_entry), module: self.shader,
buffers: vertex_buffers, entry_point: Some(vs_entry),
compilation_options: wgpu::PipelineCompilationOptions::default(), buffers: vertex_buffers,
}, compilation_options:
fragment: Some(wgpu::FragmentState { wgpu::PipelineCompilationOptions::default(),
module: self.shader, },
entry_point: Some(fs_entry), fragment: Some(wgpu::FragmentState {
targets: &[Some(wgpu::ColorTargetState { module: self.shader,
format: self.format, entry_point: Some(fs_entry),
blend: Some(blend), targets: &[Some(wgpu::ColorTargetState {
write_mask: wgpu::ColorWrites::ALL, format: self.format,
})], blend: Some(blend),
compilation_options: wgpu::PipelineCompilationOptions::default(), write_mask: wgpu::ColorWrites::ALL,
}), })],
primitive: wgpu::PrimitiveState { compilation_options:
topology, wgpu::PipelineCompilationOptions::default(),
strip_index_format: None, }),
front_face: wgpu::FrontFace::Ccw, primitive: wgpu::PrimitiveState {
cull_mode: None, topology,
polygon_mode: wgpu::PolygonMode::Fill, strip_index_format: None,
unclipped_depth: false, front_face: wgpu::FrontFace::Ccw,
conservative: false, cull_mode: None,
}, polygon_mode: wgpu::PolygonMode::Fill,
depth_stencil: None, unclipped_depth: false,
multisample: wgpu::MultisampleState::default(), conservative: false,
multiview_mask: None, },
cache: None, depth_stencil: None,
}) multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
} }
} }
+32 -17
View File
@@ -1,8 +1,8 @@
//! PTY (pseudo-terminal) handling for shell communication. //! PTY (pseudo-terminal) handling for shell communication.
use rustix::fs::{fcntl_setfl, OFlags}; use rustix::fs::{OFlags, fcntl_setfl};
use rustix::io::{read, write, Errno}; use rustix::io::{Errno, read, write};
use rustix::pty::{grantpt, openpt, ptsname, unlockpt, OpenptFlags}; use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt};
use std::ffi::CString; use std::ffi::CString;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
use thiserror::Error; use thiserror::Error;
@@ -36,20 +36,30 @@ pub struct Pty {
impl Pty { impl Pty {
/// Creates a new PTY and spawns a shell process. /// Creates a new PTY and spawns a shell process.
/// The initial terminal size should be provided so the shell starts with the correct dimensions. /// The initial terminal size should be provided so the shell starts with the correct dimensions.
pub fn spawn(shell: Option<&str>, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<Self, PtyError> { pub fn spawn(
shell: Option<&str>,
cols: u16,
rows: u16,
xpixel: u16,
ypixel: u16,
) -> Result<Self, PtyError> {
// Open the PTY master // Open the PTY master
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC) let master = openpt(
.map_err(PtyError::OpenMaster)?; OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
)
.map_err(PtyError::OpenMaster)?;
// Set non-blocking mode on master // Set non-blocking mode on master
fcntl_setfl(&master, OFlags::NONBLOCK).map_err(|e| PtyError::Io(e.into()))?; fcntl_setfl(&master, OFlags::NONBLOCK)
.map_err(|e| PtyError::Io(e.into()))?;
// Grant and unlock the PTY // Grant and unlock the PTY
grantpt(&master).map_err(PtyError::Grant)?; grantpt(&master).map_err(PtyError::Grant)?;
unlockpt(&master).map_err(PtyError::Unlock)?; unlockpt(&master).map_err(PtyError::Unlock)?;
// Get the slave name // Get the slave name
let slave_name = ptsname(&master, Vec::new()).map_err(PtyError::PtsName)?; let slave_name =
ptsname(&master, Vec::new()).map_err(PtyError::PtsName)?;
// Set the terminal size BEFORE forking so the child inherits the correct size. // Set the terminal size BEFORE forking so the child inherits the correct size.
// This prevents race conditions where the shell's .zshrc runs before the parent // This prevents race conditions where the shell's .zshrc runs before the parent
@@ -78,7 +88,8 @@ impl Pty {
} }
pid => { pid => {
// Parent process // Parent process
let child_pid = unsafe { rustix::process::Pid::from_raw_unchecked(pid) }; let child_pid =
unsafe { rustix::process::Pid::from_raw_unchecked(pid) };
Ok(Self { master, child_pid }) Ok(Self { master, child_pid })
} }
} }
@@ -125,14 +136,16 @@ impl Pty {
.or_else(|| std::env::var("SHELL").ok()) .or_else(|| std::env::var("SHELL").ok())
.unwrap_or_else(|| "/bin/sh".to_string()); .unwrap_or_else(|| "/bin/sh".to_string());
let shell_cstr = CString::new(shell_path.clone()).expect("Invalid shell path"); let shell_cstr =
CString::new(shell_path.clone()).expect("Invalid shell path");
let shell_name = std::path::Path::new(&shell_path) let shell_name = std::path::Path::new(&shell_path)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("sh"); .unwrap_or("sh");
// Login shell (prepend with -) // Login shell (prepend with -)
let login_shell = CString::new(format!("-{}", shell_name)).expect("Invalid shell name"); let login_shell = CString::new(format!("-{}", shell_name))
.expect("Invalid shell name");
// Execute the shell // Execute the shell
let args = [login_shell.as_ptr(), std::ptr::null()]; let args = [login_shell.as_ptr(), std::ptr::null()];
@@ -166,7 +179,13 @@ impl Pty {
} }
/// Resizes the PTY window. /// Resizes the PTY window.
pub fn resize(&self, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<(), PtyError> { pub fn resize(
&self,
cols: u16,
rows: u16,
xpixel: u16,
ypixel: u16,
) -> Result<(), PtyError> {
let winsize = libc::winsize { let winsize = libc::winsize {
ws_row: rows, ws_row: rows,
ws_col: cols, ws_col: cols,
@@ -210,11 +229,7 @@ impl Pty {
pub fn foreground_pgid(&self) -> Option<i32> { pub fn foreground_pgid(&self) -> Option<i32> {
let fd = self.master.as_raw_fd(); let fd = self.master.as_raw_fd();
let pgid = unsafe { libc::tcgetpgrp(fd) }; let pgid = unsafe { libc::tcgetpgrp(fd) };
if pgid > 0 { if pgid > 0 { Some(pgid) } else { None }
Some(pgid)
} else {
None
}
} }
/// Get the name of the foreground process running in this PTY. /// Get the name of the foreground process running in this PTY.
+2176 -1027
View File
File diff suppressed because it is too large Load Diff
+315 -95
View File
@@ -53,7 +53,8 @@ impl SimdCapabilities {
} }
// Global cached capabilities (initialized on first use) // Global cached capabilities (initialized on first use)
static SIMD_CAPS: std::sync::OnceLock<SimdCapabilities> = std::sync::OnceLock::new(); static SIMD_CAPS: std::sync::OnceLock<SimdCapabilities> =
std::sync::OnceLock::new();
/// Get cached SIMD capabilities. /// Get cached SIMD capabilities.
pub fn simd_caps() -> &'static SimdCapabilities { pub fn simd_caps() -> &'static SimdCapabilities {
@@ -156,7 +157,11 @@ unsafe fn find_byte_avx2(haystack: &[u8], needle: u8) -> Option<usize> {
/// ///
/// This is equivalent to Kitty's `find_either_of_two_bytes` function. /// This is equivalent to Kitty's `find_either_of_two_bytes` function.
#[inline] #[inline]
pub fn find_either_of_two_bytes(haystack: &[u8], a: u8, b: u8) -> Option<usize> { pub fn find_either_of_two_bytes(
haystack: &[u8],
a: u8,
b: u8,
) -> Option<usize> {
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
{ {
let caps = simd_caps(); let caps = simd_caps();
@@ -174,14 +179,22 @@ pub fn find_either_of_two_bytes(haystack: &[u8], a: u8, b: u8) -> Option<usize>
/// Scalar fallback for find_either_of_two_bytes. /// Scalar fallback for find_either_of_two_bytes.
#[inline] #[inline]
fn find_either_of_two_bytes_scalar(haystack: &[u8], a: u8, b: u8) -> Option<usize> { fn find_either_of_two_bytes_scalar(
haystack: &[u8],
a: u8,
b: u8,
) -> Option<usize> {
haystack.iter().position(|&byte| byte == a || byte == b) haystack.iter().position(|&byte| byte == a || byte == b)
} }
/// SSE implementation of find_either_of_two_bytes. /// SSE implementation of find_either_of_two_bytes.
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "sse2", enable = "sse4.1")] #[target_feature(enable = "sse2", enable = "sse4.1")]
unsafe fn find_either_of_two_bytes_sse(haystack: &[u8], a: u8, b: u8) -> Option<usize> { unsafe fn find_either_of_two_bytes_sse(
haystack: &[u8],
a: u8,
b: u8,
) -> Option<usize> {
let a_vec = _mm_set1_epi8(a as i8); let a_vec = _mm_set1_epi8(a as i8);
let b_vec = _mm_set1_epi8(b as i8); let b_vec = _mm_set1_epi8(b as i8);
@@ -215,7 +228,11 @@ unsafe fn find_either_of_two_bytes_sse(haystack: &[u8], a: u8, b: u8) -> Option<
/// AVX2 implementation of find_either_of_two_bytes. /// AVX2 implementation of find_either_of_two_bytes.
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "avx2")] #[target_feature(enable = "avx2")]
unsafe fn find_either_of_two_bytes_avx2(haystack: &[u8], a: u8, b: u8) -> Option<usize> { unsafe fn find_either_of_two_bytes_avx2(
haystack: &[u8],
a: u8,
b: u8,
) -> Option<usize> {
let a_vec = _mm256_set1_epi8(a as i8); let a_vec = _mm256_set1_epi8(a as i8);
let b_vec = _mm256_set1_epi8(b as i8); let b_vec = _mm256_set1_epi8(b as i8);
@@ -289,7 +306,9 @@ pub fn find_c0_control(haystack: &[u8]) -> Option<usize> {
/// Scalar fallback for find_c0_control. /// Scalar fallback for find_c0_control.
#[inline] #[inline]
fn find_c0_control_scalar(haystack: &[u8]) -> Option<usize> { fn find_c0_control_scalar(haystack: &[u8]) -> Option<usize> {
haystack.iter().position(|&byte| byte < 0x20 || byte == 0x7F) haystack
.iter()
.position(|&byte| byte < 0x20 || byte == 0x7F)
} }
/// SSE implementation of find_c0_control. /// SSE implementation of find_c0_control.
@@ -417,7 +436,11 @@ pub fn xor_mask(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize {
/// Scalar fallback for xor_mask. /// Scalar fallback for xor_mask.
#[inline] #[inline]
fn xor_mask_scalar(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize { fn xor_mask_scalar(
data: &mut [u8],
mask: [u8; 4],
start_offset: usize,
) -> usize {
let mut offset = start_offset; let mut offset = start_offset;
for byte in data.iter_mut() { for byte in data.iter_mut() {
*byte ^= mask[offset & 3]; *byte ^= mask[offset & 3];
@@ -429,7 +452,11 @@ fn xor_mask_scalar(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize
/// SSE implementation of xor_mask. /// SSE implementation of xor_mask.
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "sse2")] #[target_feature(enable = "sse2")]
unsafe fn xor_mask_sse(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize { unsafe fn xor_mask_sse(
data: &mut [u8],
mask: [u8; 4],
start_offset: usize,
) -> usize {
let len = data.len(); let len = data.len();
let ptr = data.as_mut_ptr(); let ptr = data.as_mut_ptr();
let mut pos = 0; let mut pos = 0;
@@ -444,10 +471,22 @@ unsafe fn xor_mask_sse(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> u
// Create 16-byte mask vector (repeat 4-byte mask 4 times) // Create 16-byte mask vector (repeat 4-byte mask 4 times)
let mask_vec = _mm_set_epi8( let mask_vec = _mm_set_epi8(
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[3] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[2] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[1] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
); );
// Process 16 bytes at a time // Process 16 bytes at a time
@@ -472,7 +511,11 @@ unsafe fn xor_mask_sse(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> u
/// AVX2 implementation of xor_mask. /// AVX2 implementation of xor_mask.
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "avx2")] #[target_feature(enable = "avx2")]
unsafe fn xor_mask_avx2(data: &mut [u8], mask: [u8; 4], start_offset: usize) -> usize { unsafe fn xor_mask_avx2(
data: &mut [u8],
mask: [u8; 4],
start_offset: usize,
) -> usize {
let len = data.len(); let len = data.len();
let ptr = data.as_mut_ptr(); let ptr = data.as_mut_ptr();
let mut pos = 0; let mut pos = 0;
@@ -487,14 +530,38 @@ unsafe fn xor_mask_avx2(data: &mut [u8], mask: [u8; 4], start_offset: usize) ->
// Create 32-byte mask vector (repeat 4-byte mask 8 times) // Create 32-byte mask vector (repeat 4-byte mask 8 times)
let mask_vec = _mm256_set_epi8( let mask_vec = _mm256_set_epi8(
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[3] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[2] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[1] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[0] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[3] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[2] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[1] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
); );
// Process 32 bytes at a time // Process 32 bytes at a time
@@ -509,10 +576,22 @@ unsafe fn xor_mask_avx2(data: &mut [u8], mask: [u8; 4], start_offset: usize) ->
// Process 16 bytes if remaining // Process 16 bytes if remaining
while pos + 16 <= len { while pos + 16 <= len {
let mask_vec_128 = _mm_set_epi8( let mask_vec_128 = _mm_set_epi8(
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[3] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[2] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[1] as i8,
mask[3] as i8, mask[2] as i8, mask[1] as i8, mask[0] as i8, mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
mask[3] as i8,
mask[2] as i8,
mask[1] as i8,
mask[0] as i8,
); );
let chunk = _mm_loadu_si128(ptr.add(pos) as *const __m128i); let chunk = _mm_loadu_si128(ptr.add(pos) as *const __m128i);
let xored = _mm_xor_si128(chunk, mask_vec_128); let xored = _mm_xor_si128(chunk, mask_vec_128);
@@ -549,20 +628,23 @@ const UTF8_REJECT: u8 = 12;
/// UTF-8 state transition table (Bjoern Hoehrmann's DFA). /// UTF-8 state transition table (Bjoern Hoehrmann's DFA).
static UTF8_DECODE_TABLE: [u8; 364] = [ static UTF8_DECODE_TABLE: [u8; 364] = [
// Character class lookup (0-255) // Character class lookup (0-255)
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9,
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 2, 2, 2, 2, 2, 2,
// State transition table 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10,
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8,
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, 8, 8, 8, 8, 8, 8, // State transition table
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12,
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24,
12,36,12,12,12,12,12,12,12,12,12,12, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12,
12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12,
12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12,
12, 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
]; ];
/// Decode a single UTF-8 byte using DFA. /// Decode a single UTF-8 byte using DFA.
@@ -604,7 +686,11 @@ impl SimdUtf8Decoder {
/// Output codepoints are written to the output buffer as u32 values. /// Output codepoints are written to the output buffer as u32 values.
/// Uses AVX2 (32 bytes at a time) if available, otherwise SSE (16 bytes). /// Uses AVX2 (32 bytes at a time) if available, otherwise SSE (16 bytes).
#[inline] #[inline]
pub fn decode_to_esc(&mut self, src: &[u8], output: &mut Vec<u32>) -> (usize, bool) { pub fn decode_to_esc(
&mut self,
src: &[u8],
output: &mut Vec<u32>,
) -> (usize, bool) {
output.clear(); output.clear();
if src.is_empty() { if src.is_empty() {
return (0, false); return (0, false);
@@ -627,7 +713,11 @@ impl SimdUtf8Decoder {
} }
/// Scalar fallback decoder. /// Scalar fallback decoder.
fn decode_to_esc_scalar(&mut self, src: &[u8], output: &mut Vec<u32>) -> (usize, bool) { fn decode_to_esc_scalar(
&mut self,
src: &[u8],
output: &mut Vec<u32>,
) -> (usize, bool) {
let mut pos = 0; let mut pos = 0;
while pos < src.len() { while pos < src.len() {
@@ -644,7 +734,11 @@ impl SimdUtf8Decoder {
pos += 1; pos += 1;
self.state.prev = self.state.cur; self.state.prev = self.state.cur;
match decode_utf8_byte(&mut self.state.cur, &mut self.state.codep, byte) { match decode_utf8_byte(
&mut self.state.cur,
&mut self.state.codep,
byte,
) {
UTF8_ACCEPT => { UTF8_ACCEPT => {
output.push(self.state.codep); output.push(self.state.codep);
} }
@@ -667,7 +761,11 @@ impl SimdUtf8Decoder {
/// Based on Kitty's simd-string-impl.h /// Based on Kitty's simd-string-impl.h
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "sse2", enable = "ssse3", enable = "sse4.1")] #[target_feature(enable = "sse2", enable = "ssse3", enable = "sse4.1")]
unsafe fn decode_to_esc_simd(&mut self, src: &[u8], output: &mut Vec<u32>) -> (usize, bool) { unsafe fn decode_to_esc_simd(
&mut self,
src: &[u8],
output: &mut Vec<u32>,
) -> (usize, bool) {
let mut num_consumed: usize = 0; let mut num_consumed: usize = 0;
// Finish any trailing sequence from previous call // Finish any trailing sequence from previous call
@@ -685,7 +783,8 @@ impl SimdUtf8Decoder {
let two = _mm_set1_epi8(2); let two = _mm_set1_epi8(2);
let three = _mm_set1_epi8(3); let three = _mm_set1_epi8(3);
let four = _mm_set1_epi8(4); let four = _mm_set1_epi8(4);
let numbered = _mm_set_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0); let numbered =
_mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
let limit = src.as_ptr().add(src.len()); let limit = src.as_ptr().add(src.len());
let mut p = src.as_ptr().add(num_consumed); let mut p = src.as_ptr().add(num_consumed);
@@ -701,7 +800,11 @@ impl SimdUtf8Decoder {
} else { } else {
// Partial load - zero-extend // Partial load - zero-extend
let mut buf = [0u8; 16]; let mut buf = [0u8; 16];
std::ptr::copy_nonoverlapping(p, buf.as_mut_ptr(), chunk_src_sz); std::ptr::copy_nonoverlapping(
p,
buf.as_mut_ptr(),
chunk_src_sz,
);
_mm_loadu_si128(buf.as_ptr() as *const __m128i) _mm_loadu_si128(buf.as_ptr() as *const __m128i)
}; };
@@ -712,7 +815,9 @@ impl SimdUtf8Decoder {
let esc_cmp = _mm_cmpeq_epi8(vec, esc_vec); let esc_cmp = _mm_cmpeq_epi8(vec, esc_vec);
let num_bytes_to_first_esc = Self::bytes_to_first_match(esc_cmp); let num_bytes_to_first_esc = Self::bytes_to_first_match(esc_cmp);
if num_bytes_to_first_esc >= 0 && (num_bytes_to_first_esc as usize) < chunk_src_sz { if num_bytes_to_first_esc >= 0
&& (num_bytes_to_first_esc as usize) < chunk_src_sz
{
sentinel_found = true; sentinel_found = true;
chunk_src_sz = num_bytes_to_first_esc as usize; chunk_src_sz = num_bytes_to_first_esc as usize;
num_consumed += chunk_src_sz + 1; // +1 for ESC num_consumed += chunk_src_sz + 1; // +1 for ESC
@@ -754,16 +859,37 @@ impl SimdUtf8Decoder {
let mut state = state_80; let mut state = state_80;
// 2-byte sequence starters (0xC0-0xDF, but 0xC0-0xC1 invalid) // 2-byte sequence starters (0xC0-0xDF, but 0xC0-0xC1 invalid)
let c2_start = _mm_cmplt_epi8(_mm_set1_epi8((0xC0 - 1 - 0x80) as i8), vec_signed); let c2_start = _mm_cmplt_epi8(
state = _mm_blendv_epi8(state, _mm_set1_epi8(0xC2u8 as i8), c2_start); _mm_set1_epi8((0xC0 - 1 - 0x80) as i8),
vec_signed,
);
state = _mm_blendv_epi8(
state,
_mm_set1_epi8(0xC2u8 as i8),
c2_start,
);
// 3-byte sequence starters (0xE0-0xEF) // 3-byte sequence starters (0xE0-0xEF)
let e3_start = _mm_cmplt_epi8(_mm_set1_epi8((0xE0 - 1 - 0x80) as i8), vec_signed); let e3_start = _mm_cmplt_epi8(
state = _mm_blendv_epi8(state, _mm_set1_epi8(0xE3u8 as i8), e3_start); _mm_set1_epi8((0xE0 - 1 - 0x80) as i8),
vec_signed,
);
state = _mm_blendv_epi8(
state,
_mm_set1_epi8(0xE3u8 as i8),
e3_start,
);
// 4-byte sequence starters (0xF0-0xFF, but 0xF5+ invalid) // 4-byte sequence starters (0xF0-0xFF, but 0xF5+ invalid)
let f4_start = _mm_cmplt_epi8(_mm_set1_epi8((0xF0 - 1 - 0x80) as i8), vec_signed); let f4_start = _mm_cmplt_epi8(
state = _mm_blendv_epi8(state, _mm_set1_epi8(0xF4u8 as i8), f4_start); _mm_set1_epi8((0xF0 - 1 - 0x80) as i8),
vec_signed,
);
state = _mm_blendv_epi8(
state,
_mm_set1_epi8(0xF4u8 as i8),
f4_start,
);
// mask = upper 5 bits of state (indicates byte type) // mask = upper 5 bits of state (indicates byte type)
let mask = _mm_and_si128(state, _mm_set1_epi8(0xF8u8 as i8)); let mask = _mm_and_si128(state, _mm_set1_epi8(0xF8u8 as i8));
@@ -774,9 +900,13 @@ impl SimdUtf8Decoder {
// count_subs1[i] = count[i] - 1, saturating // count_subs1[i] = count[i] - 1, saturating
let count_subs1 = _mm_subs_epu8(count, one); let count_subs1 = _mm_subs_epu8(count, one);
// counts[i] = count[i] + count_subs1[i-1] // counts[i] = count[i] + count_subs1[i-1]
let mut counts = _mm_add_epi8(count, _mm_srli_si128(count_subs1, 1)); let mut counts =
_mm_add_epi8(count, _mm_srli_si128(count_subs1, 1));
// counts[i] += counts_subs2[i-2] (for 3 and 4 byte sequences) // counts[i] += counts_subs2[i-2] (for 3 and 4 byte sequences)
counts = _mm_add_epi8(counts, _mm_srli_si128(_mm_subs_epu8(counts, two), 2)); counts = _mm_add_epi8(
counts,
_mm_srli_si128(_mm_subs_epu8(counts, two), 2),
);
// Check for trailing incomplete sequence // Check for trailing incomplete sequence
if check_for_trailing { if check_for_trailing {
@@ -789,12 +919,19 @@ impl SimdUtf8Decoder {
// We have a trailing incomplete sequence // We have a trailing incomplete sequence
check_for_trailing = false; check_for_trailing = false;
let last_byte = *start_of_current_chunk.add(chunk_src_sz - 1); let last_byte =
*start_of_current_chunk.add(chunk_src_sz - 1);
if last_byte >= 0xC0 { if last_byte >= 0xC0 {
num_trailing_bytes = 1; num_trailing_bytes = 1;
} else if chunk_src_sz > 1 && *start_of_current_chunk.add(chunk_src_sz - 2) >= 0xE0 { } else if chunk_src_sz > 1
&& *start_of_current_chunk.add(chunk_src_sz - 2)
>= 0xE0
{
num_trailing_bytes = 2; num_trailing_bytes = 2;
} else if chunk_src_sz > 2 && *start_of_current_chunk.add(chunk_src_sz - 3) >= 0xF0 { } else if chunk_src_sz > 2
&& *start_of_current_chunk.add(chunk_src_sz - 3)
>= 0xF0
{
num_trailing_bytes = 3; num_trailing_bytes = 3;
} }
@@ -805,7 +942,7 @@ impl SimdUtf8Decoder {
// Fall back to scalar for trailing bytes // Fall back to scalar for trailing bytes
let slice = std::slice::from_raw_parts( let slice = std::slice::from_raw_parts(
start_of_current_chunk, start_of_current_chunk,
num_trailing_bytes num_trailing_bytes,
); );
self.scalar_decode_all(slice, output); self.scalar_decode_all(slice, output);
num_consumed += num_trailing_bytes; num_consumed += num_trailing_bytes;
@@ -824,7 +961,7 @@ impl SimdUtf8Decoder {
// Invalid UTF-8 - fall back to scalar // Invalid UTF-8 - fall back to scalar
let slice = std::slice::from_raw_parts( let slice = std::slice::from_raw_parts(
start_of_current_chunk, start_of_current_chunk,
chunk_src_sz + num_trailing_bytes chunk_src_sz + num_trailing_bytes,
); );
self.scalar_decode_all(slice, output); self.scalar_decode_all(slice, output);
num_consumed += num_trailing_bytes; num_consumed += num_trailing_bytes;
@@ -835,53 +972,87 @@ impl SimdUtf8Decoder {
let mut chunk_invalid = zero; let mut chunk_invalid = zero;
// Validate 2-byte starters: 0xC0, 0xC1 are invalid // Validate 2-byte starters: 0xC0, 0xC1 are invalid
chunk_invalid = _mm_or_si128(chunk_invalid, chunk_invalid = _mm_or_si128(
_mm_and_si128(c2_start, _mm_cmplt_epi8(vec, _mm_set1_epi8(0xC2u8 as i8)))); chunk_invalid,
_mm_and_si128(
c2_start,
_mm_cmplt_epi8(vec, _mm_set1_epi8(0xC2u8 as i8)),
),
);
// Validate 4-byte starters: 0xF5+ are invalid // Validate 4-byte starters: 0xF5+ are invalid
chunk_invalid = _mm_or_si128(chunk_invalid, chunk_invalid = _mm_or_si128(
_mm_and_si128(f4_start, _mm_cmpgt_epi8(vec, _mm_set1_epi8(0xF4u8 as i8)))); chunk_invalid,
_mm_and_si128(
f4_start,
_mm_cmpgt_epi8(vec, _mm_set1_epi8(0xF4u8 as i8)),
),
);
// Validate continuation bytes don't have starter bytes // Validate continuation bytes don't have starter bytes
let cont_has_starter = _mm_andnot_si128( let cont_has_starter = _mm_andnot_si128(
_mm_cmplt_epi8(vec, _mm_set1_epi8(0xC0u8 as i8)), _mm_cmplt_epi8(vec, _mm_set1_epi8(0xC0u8 as i8)),
_mm_cmpgt_epi8(counts, count) _mm_cmpgt_epi8(counts, count),
); );
chunk_invalid = _mm_or_si128(chunk_invalid, cont_has_starter); chunk_invalid = _mm_or_si128(chunk_invalid, cont_has_starter);
// Validate E0 second bytes (must be >= 0xA0) // Validate E0 second bytes (must be >= 0xA0)
let e0_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xE0u8 as i8)); let e0_starters =
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xE0u8 as i8));
let e0_followers = _mm_srli_si128(e0_starters, 1); let e0_followers = _mm_srli_si128(e0_starters, 1);
let e0_invalid = _mm_and_si128(e0_followers, let e0_invalid = _mm_and_si128(
_mm_cmplt_epi8(_mm_and_si128(e0_followers, vec), _mm_set1_epi8(0xA0u8 as i8))); e0_followers,
_mm_cmplt_epi8(
_mm_and_si128(e0_followers, vec),
_mm_set1_epi8(0xA0u8 as i8),
),
);
chunk_invalid = _mm_or_si128(chunk_invalid, e0_invalid); chunk_invalid = _mm_or_si128(chunk_invalid, e0_invalid);
// Validate ED second bytes (must be < 0xA0, i.e. <= 0x9F) // Validate ED second bytes (must be < 0xA0, i.e. <= 0x9F)
let ed_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xEDu8 as i8)); let ed_starters =
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xEDu8 as i8));
let ed_followers = _mm_srli_si128(ed_starters, 1); let ed_followers = _mm_srli_si128(ed_starters, 1);
let ed_invalid = _mm_and_si128(ed_followers, let ed_invalid = _mm_and_si128(
_mm_cmpgt_epi8(_mm_and_si128(ed_followers, vec), _mm_set1_epi8(0x9Fu8 as i8))); ed_followers,
_mm_cmpgt_epi8(
_mm_and_si128(ed_followers, vec),
_mm_set1_epi8(0x9Fu8 as i8),
),
);
chunk_invalid = _mm_or_si128(chunk_invalid, ed_invalid); chunk_invalid = _mm_or_si128(chunk_invalid, ed_invalid);
// Validate F0 second bytes (must be >= 0x90) // Validate F0 second bytes (must be >= 0x90)
let f0_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF0u8 as i8)); let f0_starters =
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF0u8 as i8));
let f0_followers = _mm_srli_si128(f0_starters, 1); let f0_followers = _mm_srli_si128(f0_starters, 1);
let f0_invalid = _mm_and_si128(f0_followers, let f0_invalid = _mm_and_si128(
_mm_cmplt_epi8(_mm_and_si128(f0_followers, vec), _mm_set1_epi8(0x90u8 as i8))); f0_followers,
_mm_cmplt_epi8(
_mm_and_si128(f0_followers, vec),
_mm_set1_epi8(0x90u8 as i8),
),
);
chunk_invalid = _mm_or_si128(chunk_invalid, f0_invalid); chunk_invalid = _mm_or_si128(chunk_invalid, f0_invalid);
// Validate F4 second bytes (must be < 0x90, i.e. <= 0x8F) // Validate F4 second bytes (must be < 0x90, i.e. <= 0x8F)
let f4_starters = _mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF4u8 as i8)); let f4_starters =
_mm_cmpeq_epi8(vec, _mm_set1_epi8(0xF4u8 as i8));
let f4_followers = _mm_srli_si128(f4_starters, 1); let f4_followers = _mm_srli_si128(f4_starters, 1);
let f4_invalid = _mm_and_si128(f4_followers, let f4_invalid = _mm_and_si128(
_mm_cmpgt_epi8(_mm_and_si128(f4_followers, vec), _mm_set1_epi8(0x8Fu8 as i8))); f4_followers,
_mm_cmpgt_epi8(
_mm_and_si128(f4_followers, vec),
_mm_set1_epi8(0x8Fu8 as i8),
),
);
chunk_invalid = _mm_or_si128(chunk_invalid, f4_invalid); chunk_invalid = _mm_or_si128(chunk_invalid, f4_invalid);
// If invalid, fall back to scalar // If invalid, fall back to scalar
if _mm_testz_si128(chunk_invalid, chunk_invalid) == 0 { if _mm_testz_si128(chunk_invalid, chunk_invalid) == 0 {
let slice = std::slice::from_raw_parts( let slice = std::slice::from_raw_parts(
start_of_current_chunk, start_of_current_chunk,
chunk_src_sz + num_trailing_bytes chunk_src_sz + num_trailing_bytes,
); );
self.scalar_decode_all(slice, output); self.scalar_decode_all(slice, output);
num_consumed += num_trailing_bytes; num_consumed += num_trailing_bytes;
@@ -892,16 +1063,21 @@ impl SimdUtf8Decoder {
vec = _mm_andnot_si128(mask, vec); vec = _mm_andnot_si128(mask, vec);
// Build output vectors // Build output vectors
let vec_non_ascii = _mm_andnot_si128(_mm_cmpeq_epi8(counts, zero), vec); let vec_non_ascii =
_mm_andnot_si128(_mm_cmpeq_epi8(counts, zero), vec);
// output1: lowest byte of each codepoint // output1: lowest byte of each codepoint
// For count==1 positions: OR with shifted bits from count==2 position // For count==1 positions: OR with shifted bits from count==2 position
let count1_locs = _mm_cmpeq_epi8(counts, one); let count1_locs = _mm_cmpeq_epi8(counts, one);
let shifted_6 = _mm_and_si128( let shifted_6 = _mm_and_si128(
_mm_slli_epi16(_mm_srli_si128(vec_non_ascii, 1), 6), _mm_slli_epi16(_mm_srli_si128(vec_non_ascii, 1), 6),
_mm_set1_epi8(0xC0u8 as i8) _mm_set1_epi8(0xC0u8 as i8),
);
let output1 = _mm_blendv_epi8(
vec,
_mm_or_si128(vec, shifted_6),
count1_locs,
); );
let output1 = _mm_blendv_epi8(vec, _mm_or_si128(vec, shifted_6), count1_locs);
// output2: middle byte (for 3 and 4 byte sequences) // output2: middle byte (for 3 and 4 byte sequences)
let count2_locs = _mm_cmpeq_epi8(counts, two); let count2_locs = _mm_cmpeq_epi8(counts, two);
@@ -910,7 +1086,13 @@ impl SimdUtf8Decoder {
output2 = _mm_srli_epi32(output2, 2); // bits 5,4,3,2 output2 = _mm_srli_epi32(output2, 2); // bits 5,4,3,2
let shifted_4 = _mm_and_si128( let shifted_4 = _mm_and_si128(
_mm_set1_epi8(0xF0u8 as i8), _mm_set1_epi8(0xF0u8 as i8),
_mm_slli_epi16(_mm_srli_si128(_mm_and_si128(count3_locs, vec_non_ascii), 1), 4) _mm_slli_epi16(
_mm_srli_si128(
_mm_and_si128(count3_locs, vec_non_ascii),
1,
),
4,
),
); );
output2 = _mm_or_si128(output2, shifted_4); output2 = _mm_or_si128(output2, shifted_4);
output2 = _mm_and_si128(output2, count2_locs); output2 = _mm_and_si128(output2, count2_locs);
@@ -921,7 +1103,13 @@ impl SimdUtf8Decoder {
let mut output3 = _mm_and_si128(three, _mm_srli_epi32(vec, 4)); // bits 5,6 from count==3 let mut output3 = _mm_and_si128(three, _mm_srli_epi32(vec, 4)); // bits 5,6 from count==3
let shifted_2 = _mm_and_si128( let shifted_2 = _mm_and_si128(
_mm_set1_epi8(0xFCu8 as i8), _mm_set1_epi8(0xFCu8 as i8),
_mm_slli_epi16(_mm_srli_si128(_mm_and_si128(count4_locs, vec_non_ascii), 1), 2) _mm_slli_epi16(
_mm_srli_si128(
_mm_and_si128(count4_locs, vec_non_ascii),
1,
),
2,
),
); );
output3 = _mm_or_si128(output3, shifted_2); output3 = _mm_or_si128(output3, shifted_2);
output3 = _mm_and_si128(output3, count3_locs); output3 = _mm_and_si128(output3, count3_locs);
@@ -959,7 +1147,13 @@ impl SimdUtf8Decoder {
let num_codepoints = chunk_src_sz - num_discarded; let num_codepoints = chunk_src_sz - num_discarded;
// Output unicode codepoints // Output unicode codepoints
Self::output_unicode(output1, output2, output3, num_codepoints, output); Self::output_unicode(
output1,
output2,
output3,
num_codepoints,
output,
);
// Handle trailing bytes // Handle trailing bytes
if num_trailing_bytes > 0 && p < limit { if num_trailing_bytes > 0 && p < limit {
@@ -1074,7 +1268,11 @@ impl SimdUtf8Decoder {
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
#[target_feature(enable = "sse2", enable = "sse4.1")] #[target_feature(enable = "sse2", enable = "sse4.1")]
#[inline] #[inline]
unsafe fn output_plain_ascii(vec: __m128i, src_sz: usize, output: &mut Vec<u32>) { unsafe fn output_plain_ascii(
vec: __m128i,
src_sz: usize,
output: &mut Vec<u32>,
) {
output.reserve(src_sz); output.reserve(src_sz);
// Process 4 bytes at a time // Process 4 bytes at a time
@@ -1102,7 +1300,7 @@ impl SimdUtf8Decoder {
output2: __m128i, output2: __m128i,
output3: __m128i, output3: __m128i,
num_codepoints: usize, num_codepoints: usize,
output: &mut Vec<u32> output: &mut Vec<u32>,
) { ) {
output.reserve(num_codepoints); output.reserve(num_codepoints);
@@ -1121,7 +1319,8 @@ impl SimdUtf8Decoder {
let unpacked3 = _mm_cvtepu8_epi32(_mm_srli_si128(o3, 0)); let unpacked3 = _mm_cvtepu8_epi32(_mm_srli_si128(o3, 0));
let unpacked3 = _mm_slli_epi32(unpacked3, 16); let unpacked3 = _mm_slli_epi32(unpacked3, 16);
let unpacked = _mm_or_si128(_mm_or_si128(unpacked1, unpacked2), unpacked3); let unpacked =
_mm_or_si128(_mm_or_si128(unpacked1, unpacked2), unpacked3);
let to_write = remaining.min(4); let to_write = remaining.min(4);
let mut buf = [0u32; 4]; let mut buf = [0u32; 4];
@@ -1136,7 +1335,11 @@ impl SimdUtf8Decoder {
} }
/// Scalar decode until state is ACCEPT. /// Scalar decode until state is ACCEPT.
fn scalar_decode_to_accept(&mut self, src: &[u8], output: &mut Vec<u32>) -> usize { fn scalar_decode_to_accept(
&mut self,
src: &[u8],
output: &mut Vec<u32>,
) -> usize {
let mut pos = 0; let mut pos = 0;
while pos < src.len() && self.state.cur != UTF8_ACCEPT { while pos < src.len() && self.state.cur != UTF8_ACCEPT {
let byte = src[pos]; let byte = src[pos];
@@ -1147,7 +1350,11 @@ impl SimdUtf8Decoder {
} }
pos += 1; pos += 1;
self.state.prev = self.state.cur; self.state.prev = self.state.cur;
match decode_utf8_byte(&mut self.state.cur, &mut self.state.codep, byte) { match decode_utf8_byte(
&mut self.state.cur,
&mut self.state.codep,
byte,
) {
UTF8_ACCEPT => output.push(self.state.codep), UTF8_ACCEPT => output.push(self.state.codep),
UTF8_REJECT => { UTF8_REJECT => {
output.push(0xFFFD); output.push(0xFFFD);
@@ -1164,7 +1371,11 @@ impl SimdUtf8Decoder {
} }
/// Scalar decode all bytes. /// Scalar decode all bytes.
fn scalar_decode_all(&mut self, src: &[u8], output: &mut Vec<u32>) -> usize { fn scalar_decode_all(
&mut self,
src: &[u8],
output: &mut Vec<u32>,
) -> usize {
let mut pos = 0; let mut pos = 0;
while pos < src.len() { while pos < src.len() {
let byte = src[pos]; let byte = src[pos];
@@ -1177,7 +1388,11 @@ impl SimdUtf8Decoder {
} }
pos += 1; pos += 1;
self.state.prev = self.state.cur; self.state.prev = self.state.cur;
match decode_utf8_byte(&mut self.state.cur, &mut self.state.codep, byte) { match decode_utf8_byte(
&mut self.state.cur,
&mut self.state.codep,
byte,
) {
UTF8_ACCEPT => output.push(self.state.codep), UTF8_ACCEPT => output.push(self.state.codep),
UTF8_REJECT => { UTF8_REJECT => {
output.push(0xFFFD); output.push(0xFFFD);
@@ -1222,7 +1437,8 @@ mod tests {
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output); let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
assert_eq!(consumed, 13); assert_eq!(consumed, 13);
assert!(!found_esc); assert!(!found_esc);
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect(); let chars: Vec<char> =
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
assert_eq!(chars.iter().collect::<String>(), "Hello, World!"); assert_eq!(chars.iter().collect::<String>(), "Hello, World!");
} }
@@ -1234,7 +1450,8 @@ mod tests {
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output); let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
assert_eq!(consumed, 6); // "Hello" + ESC assert_eq!(consumed, 6); // "Hello" + ESC
assert!(found_esc); assert!(found_esc);
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect(); let chars: Vec<char> =
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
assert_eq!(chars.iter().collect::<String>(), "Hello"); assert_eq!(chars.iter().collect::<String>(), "Hello");
} }
@@ -1246,7 +1463,8 @@ mod tests {
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output); let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
assert_eq!(consumed, 5); // c, a, f, é (2 bytes) assert_eq!(consumed, 5); // c, a, f, é (2 bytes)
assert!(!found_esc); assert!(!found_esc);
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect(); let chars: Vec<char> =
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
assert_eq!(chars.iter().collect::<String>(), "café"); assert_eq!(chars.iter().collect::<String>(), "café");
} }
@@ -1258,7 +1476,8 @@ mod tests {
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output); let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
assert_eq!(consumed, 9); // 3 chars * 3 bytes assert_eq!(consumed, 9); // 3 chars * 3 bytes
assert!(!found_esc); assert!(!found_esc);
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect(); let chars: Vec<char> =
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
assert_eq!(chars.iter().collect::<String>(), "日本語"); assert_eq!(chars.iter().collect::<String>(), "日本語");
} }
@@ -1270,7 +1489,8 @@ mod tests {
let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output); let (consumed, found_esc) = decoder.decode_to_esc(input, &mut output);
assert_eq!(consumed, 8); // 2 chars * 4 bytes assert_eq!(consumed, 8); // 2 chars * 4 bytes
assert!(!found_esc); assert!(!found_esc);
let chars: Vec<char> = output.iter().filter_map(|&cp| char::from_u32(cp)).collect(); let chars: Vec<char> =
output.iter().filter_map(|&cp| char::from_u32(cp)).collect();
assert_eq!(chars.iter().collect::<String>(), "🎉🚀"); assert_eq!(chars.iter().collect::<String>(), "🎉🚀");
} }
+4 -1
View File
@@ -113,7 +113,10 @@ impl StatuslineSection {
} }
/// Add multiple components to this section. /// Add multiple components to this section.
pub fn with_components(mut self, components: Vec<StatuslineComponent>) -> Self { pub fn with_components(
mut self,
components: Vec<StatuslineComponent>,
) -> Self {
self.components = components; self.components = components;
self self
} }
+112 -37
View File
@@ -1,7 +1,7 @@
//! Terminal state management and escape sequence handling. //! Terminal state management and escape sequence handling.
use crate::graphics::{GraphicsCommand, ImageStorage}; use crate::graphics::{GraphicsCommand, ImageStorage};
use crate::keyboard::{query_response, KeyboardState}; use crate::keyboard::{KeyboardState, query_response};
use crate::vt_parser::{CsiParams, Handler}; use crate::vt_parser::{CsiParams, Handler};
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
@@ -287,8 +287,7 @@ struct SavedCursor {
} }
/// Alternate screen buffer state. /// Alternate screen buffer state.
#[derive(Clone)] pub struct AlternateScreen {
struct AlternateScreen {
grid: Vec<Vec<Cell>>, grid: Vec<Vec<Cell>>,
line_map: Vec<usize>, line_map: Vec<usize>,
cursor_col: usize, cursor_col: usize,
@@ -296,6 +295,7 @@ struct AlternateScreen {
saved_cursor: SavedCursor, saved_cursor: SavedCursor,
scroll_top: usize, scroll_top: usize,
scroll_bottom: usize, scroll_bottom: usize,
pub image_storage: ImageStorage,
} }
/// Kitty-style ring buffer for scrollback history. /// Kitty-style ring buffer for scrollback history.
@@ -418,7 +418,7 @@ pub struct Terminal {
pub grid: Vec<Vec<Cell>>, pub grid: Vec<Vec<Cell>>,
/// Maps visual row index to actual grid row index. /// Maps visual row index to actual grid row index.
/// This allows O(1) scrolling by rotating indices instead of moving cells. /// This allows O(1) scrolling by rotating indices instead of moving cells.
line_map: Vec<usize>, pub line_map: Vec<usize>,
/// Number of columns. /// Number of columns.
pub cols: usize, pub cols: usize,
/// Number of rows. /// Number of rows.
@@ -472,7 +472,7 @@ pub struct Terminal {
/// Saved cursor state (DECSC/DECRC). /// Saved cursor state (DECSC/DECRC).
saved_cursor: SavedCursor, saved_cursor: SavedCursor,
/// Alternate screen buffer (for fullscreen apps like vim, less). /// Alternate screen buffer (for fullscreen apps like vim, less).
alternate_screen: Option<AlternateScreen>, pub alternate_screen: Option<AlternateScreen>,
/// Whether we're currently using the alternate screen. /// Whether we're currently using the alternate screen.
pub using_alternate_screen: bool, pub using_alternate_screen: bool,
/// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [. /// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [.
@@ -584,7 +584,10 @@ impl Terminal {
/// Check if any line is dirty. /// Check if any line is dirty.
#[inline] #[inline]
pub fn has_any_dirty_line(&self) -> bool { 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 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.
@@ -775,16 +778,19 @@ impl Terminal {
return; // Already in alternate screen return; // Already in alternate screen
} }
// Save main screen state // Create alternate screen if it doesn't exist, otherwise reuse it
self.alternate_screen = Some(AlternateScreen { if self.alternate_screen.is_none() {
grid: self.grid.clone(), self.alternate_screen = Some(AlternateScreen {
line_map: self.line_map.clone(), grid: self.grid.clone(),
cursor_col: self.cursor_col, line_map: self.line_map.clone(),
cursor_row: self.cursor_row, cursor_col: self.cursor_col,
saved_cursor: self.saved_cursor.clone(), cursor_row: self.cursor_row,
scroll_top: self.scroll_top, saved_cursor: self.saved_cursor.clone(),
scroll_bottom: self.scroll_bottom, scroll_top: self.scroll_top,
}); scroll_bottom: self.scroll_bottom,
image_storage: ImageStorage::new(),
});
}
// Clear the screen for alternate buffer // Clear the screen for alternate buffer
self.grid = vec![vec![Cell::default(); self.cols]; self.rows]; self.grid = vec![vec![Cell::default(); self.cols]; self.rows];
@@ -801,8 +807,14 @@ impl Terminal {
self.dirty = true; self.dirty = true;
log::debug!( log::debug!(
"Entered alternate screen buffer: rows={}, cols={}, scroll_region={}-{}, dirty_lines={:016x}{:016x}{:016x}{:016x}", "Entered alternate screen buffer: rows={}, cols={}, scroll_region={}-{}, dirty_lines={:016x}{:016x}{:016x}{:016x}",
self.rows, self.cols, self.scroll_top, self.scroll_bottom, self.rows,
self.dirty_lines[3], self.dirty_lines[2], self.dirty_lines[1], self.dirty_lines[0] self.cols,
self.scroll_top,
self.scroll_bottom,
self.dirty_lines[3],
self.dirty_lines[2],
self.dirty_lines[1],
self.dirty_lines[0]
); );
} }
@@ -812,10 +824,10 @@ impl Terminal {
return; // Not in alternate screen return; // Not in alternate screen
} }
if let Some(saved) = self.alternate_screen.take() { if let Some(saved) = self.alternate_screen.as_ref() {
self.grid = saved.grid; self.grid = saved.grid.clone();
self.line_map = saved.line_map; self.line_map = saved.line_map.clone();
self.saved_cursor = saved.saved_cursor; self.saved_cursor = saved.saved_cursor.clone();
self.scroll_top = saved.scroll_top; self.scroll_top = saved.scroll_top;
self.scroll_bottom = saved.scroll_bottom; self.scroll_bottom = saved.scroll_bottom;
// Clamp cursor positions to current grid dimensions (defensive) // Clamp cursor positions to current grid dimensions (defensive)
@@ -823,9 +835,11 @@ impl Terminal {
self.cursor_row = saved.cursor_row.min(self.rows.saturating_sub(1)); self.cursor_row = saved.cursor_row.min(self.rows.saturating_sub(1));
} }
// Wipe alternate screen and its image storage to reclaim memory
self.alternate_screen = None;
self.using_alternate_screen = false; self.using_alternate_screen = false;
self.mark_all_lines_dirty(); self.mark_all_lines_dirty();
log::debug!("Left alternate screen buffer"); log::debug!("Left alternate screen buffer and cleared its storage");
} }
/// Scrolls the scroll region up by n lines. /// Scrolls the scroll region up by n lines.
@@ -852,6 +866,9 @@ impl Terminal {
{ {
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic // Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
// If buffer is full, this overwrites the oldest line (perfect for our swap) // If buffer is full, this overwrites the oldest line (perfect for our swap)
if self.scrollback.is_full() {
self.image_storage.shift_placements(-1);
}
let cols = self.cols; let cols = self.cols;
let dest = self.scrollback.push(cols); let dest = self.scrollback.push(cols);
// Swap grid row content into scrollback slot // Swap grid row content into scrollback slot
@@ -859,6 +876,10 @@ impl Terminal {
std::mem::swap(&mut self.grid[recycled_grid_row], dest); std::mem::swap(&mut self.grid[recycled_grid_row], dest);
// Clear the grid row (now contains old scrollback data or empty) // Clear the grid row (now contains old scrollback data or empty)
self.clear_grid_row(recycled_grid_row); self.clear_grid_row(recycled_grid_row);
if self.scroll_offset > 0 {
self.scroll_offset =
(self.scroll_offset + 1).min(self.scrollback.capacity);
}
} else { } else {
// Not saving to scrollback - just clear the line // Not saving to scrollback - just clear the line
self.clear_grid_row(recycled_grid_row); self.clear_grid_row(recycled_grid_row);
@@ -1293,6 +1314,9 @@ impl Terminal {
for visual_row in 0..self.rows { for visual_row in 0..self.rows {
let grid_row = self.line_map[visual_row]; let grid_row = self.line_map[visual_row];
// Get a slot in the ring buffer and swap content into it // Get a slot in the ring buffer and swap content into it
if self.scrollback.is_full() {
self.image_storage.shift_placements(-1);
}
let cols = self.cols; let cols = self.cols;
let dest = self.scrollback.push(cols); let dest = self.scrollback.push(cols);
std::mem::swap(&mut self.grid[grid_row], dest); std::mem::swap(&mut self.grid[grid_row], dest);
@@ -1328,8 +1352,12 @@ impl Handler for Terminal {
.iter() .iter()
.filter_map(|&c| char::from_u32(c)) .filter_map(|&c| char::from_u32(c))
.collect(); .collect();
log::error!("DEBUG CSI LEAK: text handler received CSI-like content: {:?} at ({}, {})", log::error!(
text, self.cursor_col, self.cursor_row); "DEBUG CSI LEAK: text handler received CSI-like content: {:?} at ({}, {})",
text,
self.cursor_col,
self.cursor_row
);
} }
} }
@@ -1672,7 +1700,9 @@ impl Handler for Terminal {
b'1' => { b'1' => {
// Start pending mode (pause rendering) // Start pending mode (pause rendering)
if self.synchronized_output { if self.synchronized_output {
log::warn!("Pending mode start requested while already in pending mode"); log::warn!(
"Pending mode start requested while already in pending mode"
);
} }
self.synchronized_output = true; self.synchronized_output = true;
log::trace!("DCS pending mode started (=1s)"); log::trace!("DCS pending mode started (=1s)");
@@ -1680,7 +1710,9 @@ impl Handler for Terminal {
b'2' => { b'2' => {
// Stop pending mode (resume rendering) // Stop pending mode (resume rendering)
if !self.synchronized_output { if !self.synchronized_output {
log::warn!("Pending mode stop requested while not in pending mode"); log::warn!(
"Pending mode stop requested while not in pending mode"
);
} }
self.synchronized_output = false; self.synchronized_output = false;
self.dirty = true; // Force a redraw self.dirty = true; // Force a redraw
@@ -1736,7 +1768,7 @@ impl Handler for Terminal {
"CSI C: cursor forward {} from col {} to {}", "CSI C: cursor forward {} from col {} to {}",
n, n,
old_col, old_col,
self.cursor_col self.cursor_col,
); );
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
@@ -1774,7 +1806,7 @@ impl Handler for Terminal {
log::trace!( log::trace!(
"CSI G: cursor to col {} (was {})", "CSI G: cursor to col {} (was {})",
self.cursor_col, self.cursor_col,
old_col old_col,
); );
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
@@ -1790,6 +1822,13 @@ 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);
log::debug!(
"CSI H/f: cursor to row {}, col {} (was {}, {})",
self.cursor_row,
self.cursor_col,
self.cursor_row,
self.cursor_col
);
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
// Erase in Display // Erase in Display
@@ -2089,7 +2128,10 @@ impl Handler for Terminal {
_ => { _ => {
log::debug!( log::debug!(
"Unhandled CSI: action='{}' primary={} secondary={} params={:?}", "Unhandled CSI: action='{}' primary={} secondary={} params={:?}",
action, primary, secondary, &params.params[..params.num_params] action,
primary,
secondary,
&params.params[..params.num_params]
); );
} }
} }
@@ -2241,8 +2283,9 @@ impl Handler for Terminal {
strikethrough: false, strikethrough: false,
wide_continuation: false, wide_continuation: false,
wrapped: false, wrapped: false,
}; }
} }
self.mark_line_dirty(visual_row); self.mark_line_dirty(visual_row);
} }
} }
@@ -2721,17 +2764,45 @@ impl Terminal {
// Convert cursor_row to absolute row (accounting for scrollback) // Convert cursor_row to absolute row (accounting for scrollback)
// This allows images to scroll with terminal content // This allows images to scroll with terminal content
let absolute_row = self.scrollback.len() + self.cursor_row; let absolute_row = if self.using_alternate_screen {
self.cursor_row
} else {
self.scrollback.len() + self.cursor_row
};
// Process the command log::debug!(
let (response, placement_result) = "Routing image command to {}: cursor_col={}, absolute_row={}, using_alt={}",
if self.using_alternate_screen {
"alternate"
} else {
"main"
},
self.cursor_col,
absolute_row,
self.using_alternate_screen
);
let (response, placement_result) = if self.using_alternate_screen {
if let Some(ref mut alt) = self.alternate_screen {
alt.image_storage.process_command(
cmd,
self.cursor_col,
absolute_row,
self.cell_width,
self.cell_height,
)
} else {
// This should not happen if using_alternate_screen is true
(None, None)
}
} else {
self.image_storage.process_command( self.image_storage.process_command(
cmd, cmd,
self.cursor_col, self.cursor_col,
absolute_row, absolute_row,
self.cell_width, self.cell_width,
self.cell_height, self.cell_height,
); )
};
// Queue the response to send back to the application // Queue the response to send back to the application
if let Some(resp) = response { if let Some(resp) = response {
@@ -2744,10 +2815,11 @@ impl Terminal {
// by the number of rows in the image placement rectangle." // by the number of rows in the image placement rectangle."
// However, if C=1 was specified, don't move the cursor. // However, if C=1 was specified, don't move the cursor.
if let Some(placement) = placement_result { if let Some(placement) = placement_result {
self.dirty = true;
if !placement.suppress_cursor_move if !placement.suppress_cursor_move
&& !placement.virtual_placement && !placement.virtual_placement
{ {
// Move cursor to the right and down by the image dimensions // Move cursor to the right and and down by the image dimensions
self.cursor_col += placement.cols; self.cursor_col += placement.cols;
let new_row = self.cursor_row + placement.rows; let new_row = self.cursor_row + placement.rows;
if new_row >= self.rows { if new_row >= self.rows {
@@ -2762,7 +2834,10 @@ impl Terminal {
// cursor movement logic (wrapping/scrolling) if applicable. // cursor movement logic (wrapping/scrolling) if applicable.
log::debug!( log::debug!(
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)", "Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
self.cursor_col, self.cursor_row, placement.cols, placement.rows self.cursor_col,
self.cursor_row,
placement.cols,
placement.rows
); );
} }
} }
+5 -2
View File
@@ -550,8 +550,11 @@ impl SharedParser {
} }
} else if buffer_was_ever_full { } else if buffer_was_ever_full {
// Buffer was full but nothing consumed - stuck in partial sequence? // Buffer was full but nothing consumed - stuck in partial sequence?
log::warn!("[PARSE] Buffer was full but read_consumed=0! read_pos={} read_sz={}", log::warn!(
state.read_pos, state.read_sz); "[PARSE] Buffer was full but read_consumed=0! read_pos={} read_sz={}",
state.read_pos,
state.read_sz
);
} }
drop(state); drop(state);
+26 -6
View File
@@ -33,7 +33,10 @@ impl Handler for DummyHandler {
#[test] #[test]
fn test_osc_leak_byte_by_byte() { fn test_osc_leak_byte_by_byte() {
let parser = SharedParser::new(); let parser = SharedParser::new();
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::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\\"; let data = b"\x1b]4;1;#769E00\x1b\\\x1b]4;2;#93DE88\x1b\\";
@@ -53,14 +56,24 @@ fn test_osc_leak_byte_by_byte() {
println!(" OSC: {:?}", std::str::from_utf8(call).unwrap()); println!(" OSC: {:?}", std::str::from_utf8(call).unwrap());
} }
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!"); assert_eq!(
assert_eq!(handler.osc_calls.len(), 2, "Should have parsed exactly two OSC calls"); 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] #[test]
fn test_csi_aborted_by_osc() { fn test_csi_aborted_by_osc() {
let parser = SharedParser::new(); let parser = SharedParser::new();
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() }; let mut handler = DummyHandler {
text: String::new(),
osc_calls: Vec::new(),
};
// An incomplete CSI sequence aborted by an OSC sequence // An incomplete CSI sequence aborted by an OSC sequence
let data = b"\x1b[38;2;255;0;0\x1b]4;1;#769E00\x1b\\"; let data = b"\x1b[38;2;255;0;0\x1b]4;1;#769E00\x1b\\";
@@ -74,6 +87,13 @@ fn test_csi_aborted_by_osc() {
while parser.run_parse_pass(&mut handler) {} while parser.run_parse_pass(&mut handler) {}
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!"); assert_eq!(
assert_eq!(handler.osc_calls.len(), 1, "Should have parsed the OSC sequence even after aborting CSI"); 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"
);
} }