Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d43b024d2 | ||
|
|
67b9b11131 | ||
|
|
dd0fcbc671 | ||
|
|
c9c91b6aa2 | ||
|
|
4727e51d1b | ||
|
|
04b6776ebf |
+1
-1
@@ -87,7 +87,7 @@ flate2 = "1"
|
|||||||
|
|
||||||
# Video decoding for WebM support (video only, no audio)
|
# Video decoding for WebM support (video only, no audio)
|
||||||
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
||||||
ffmpeg-next = { version = "8.1", optional = true }
|
ffmpeg-next = { version = "9.0", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["webm"]
|
default = ["webm"]
|
||||||
|
|||||||
+31
-20
@@ -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";
|
||||||
@@ -25,17 +25,17 @@ fn random_string(len: usize, rng: &mut u64) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run a benchmark with multiple repetitions like Kitty does
|
/// Run a benchmark with multiple repetitions like Kitty does
|
||||||
fn run_benchmark<F>(name: &str, data: &[u8], repetitions: usize, mut setup: F)
|
fn run_benchmark<F>(name: &str, data: &[u8], repetitions: usize, mut setup: F)
|
||||||
where
|
where
|
||||||
F: FnMut() -> (Terminal, Parser),
|
F: FnMut() -> (Terminal, Parser),
|
||||||
{
|
{
|
||||||
let data_size = data.len();
|
let data_size = data.len();
|
||||||
let total_size = data_size * repetitions;
|
let total_size = data_size * repetitions;
|
||||||
|
|
||||||
// Warmup run
|
// Warmup run
|
||||||
let (mut terminal, mut parser) = setup();
|
let (mut terminal, mut parser) = setup();
|
||||||
parser.parse(data, &mut terminal);
|
parser.parse(data, &mut terminal);
|
||||||
|
|
||||||
// Timed runs
|
// Timed runs
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
for _ in 0..repetitions {
|
for _ in 0..repetitions {
|
||||||
@@ -43,18 +43,24 @@ where
|
|||||||
parser.parse(data, &mut terminal);
|
parser.parse(data, &mut terminal);
|
||||||
}
|
}
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
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() {
|
||||||
println!("=== ZTerm VT Parser Benchmark ===");
|
println!("=== ZTerm VT Parser Benchmark ===");
|
||||||
println!("Matching Kitty's kitten __benchmark__ methodology\n");
|
println!("Matching Kitty's kitten __benchmark__ methodology\n");
|
||||||
|
|
||||||
// Benchmark 1: Only ASCII chars (matches Kitty's simple_ascii)
|
// Benchmark 1: Only ASCII chars (matches Kitty's simple_ascii)
|
||||||
println!("--- Only ASCII chars ---");
|
println!("--- Only ASCII chars ---");
|
||||||
let target_sz = 1024 * 2048 + 13;
|
let target_sz = 1024 * 2048 + 13;
|
||||||
@@ -66,22 +72,22 @@ fn main() {
|
|||||||
let idx = ((rng >> 33) % alphabet.len() as u64) as usize;
|
let idx = ((rng >> 33) % alphabet.len() as u64) as usize;
|
||||||
ascii_data.push(alphabet[idx]);
|
ascii_data.push(alphabet[idx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
run_benchmark("Only ASCII chars", &ascii_data, REPETITIONS, || {
|
run_benchmark("Only ASCII chars", &ascii_data, REPETITIONS, || {
|
||||||
(Terminal::new(80, 25, 20000), Parser::new())
|
(Terminal::new(80, 25, 20000), Parser::new())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Benchmark 2: CSI codes with few chars (matches Kitty's ascii_with_csi)
|
// Benchmark 2: CSI codes with few chars (matches Kitty's ascii_with_csi)
|
||||||
println!("\n--- CSI codes with few chars ---");
|
println!("\n--- CSI codes with few chars ---");
|
||||||
let target_sz = 1024 * 1024 + 17;
|
let target_sz = 1024 * 1024 + 17;
|
||||||
let mut csi_data = Vec::with_capacity(target_sz + 100);
|
let mut csi_data = Vec::with_capacity(target_sz + 100);
|
||||||
let mut rng: u64 = 12345; // Fixed seed for reproducibility
|
let mut rng: u64 = 12345; // Fixed seed for reproducibility
|
||||||
|
|
||||||
while csi_data.len() < target_sz {
|
while csi_data.len() < target_sz {
|
||||||
// Simple LCG random for chunk selection
|
// Simple LCG random for chunk selection
|
||||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||||
let q = ((rng >> 33) % 100) as u32;
|
let q = ((rng >> 33) % 100) as u32;
|
||||||
|
|
||||||
match q {
|
match q {
|
||||||
0..=9 => {
|
0..=9 => {
|
||||||
// 10%: random ASCII text (1-72 chars)
|
// 10%: random ASCII text (1-72 chars)
|
||||||
@@ -111,32 +117,37 @@ 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
csi_data.extend_from_slice(b"\x1b[m");
|
csi_data.extend_from_slice(b"\x1b[m");
|
||||||
|
|
||||||
run_benchmark("CSI codes with few chars", &csi_data, REPETITIONS, || {
|
run_benchmark("CSI codes with few chars", &csi_data, REPETITIONS, || {
|
||||||
(Terminal::new(80, 25, 20000), Parser::new())
|
(Terminal::new(80, 25, 20000), Parser::new())
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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;");
|
||||||
long_esc_data.extend_from_slice(long_content.as_bytes());
|
long_esc_data.extend_from_slice(long_content.as_bytes());
|
||||||
long_esc_data.push(0x07); // BEL terminator
|
long_esc_data.push(0x07); // BEL terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
run_benchmark("Long escape codes", &long_esc_data, REPETITIONS, || {
|
run_benchmark("Long escape codes", &long_esc_data, REPETITIONS, || {
|
||||||
(Terminal::new(80, 25, 20000), Parser::new())
|
(Terminal::new(80, 25, 20000), Parser::new())
|
||||||
});
|
});
|
||||||
|
|
||||||
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)");
|
||||||
}
|
}
|
||||||
|
|||||||
+201
-90
@@ -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
|
||||||
@@ -43,7 +47,7 @@ pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
|
|||||||
|
|
||||||
// Add the charset to the pattern
|
// Add the charset to the pattern
|
||||||
FcPatternAddCharSet(pat, FC_CHARSET.as_ptr() as *const i8, charset);
|
FcPatternAddCharSet(pat, FC_CHARSET.as_ptr() as *const i8, charset);
|
||||||
|
|
||||||
// Request a color font
|
// Request a color font
|
||||||
FcPatternAddBool(pat, FC_COLOR.as_ptr() as *const i8, 1); // FcTrue = 1
|
FcPatternAddBool(pat, FC_COLOR.as_ptr() as *const i8, 1); // FcTrue = 1
|
||||||
|
|
||||||
@@ -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,
|
||||||
log::debug!("find_color_font_for_char: matched font, is_color={}", has_color);
|
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
|
||||||
|
);
|
||||||
|
|
||||||
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,35 +195,54 @@ 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
|
||||||
// and will fail. Cairo handles font sizing internally.
|
// and will fail. Cairo handles font sizing internally.
|
||||||
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?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clone the Cairo font face (it's reference-counted)
|
// Clone the Cairo font face (it's reference-counted)
|
||||||
let cairo_face = {
|
let cairo_face = {
|
||||||
let (_, cairo_face) = self.faces.get(font_path)?;
|
let (_, cairo_face) = self.faces.get(font_path)?;
|
||||||
@@ -198,19 +252,30 @@ impl ColorFontRenderer {
|
|||||||
// For emoji, we typically render at 2x cell width (double-width character)
|
// For emoji, we typically render at 2x cell width (double-width character)
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -220,9 +285,9 @@ impl ColorFontRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let surface = self.surface.as_mut()?;
|
let surface = self.surface.as_mut()?;
|
||||||
|
|
||||||
// Create Cairo context
|
// Create Cairo context
|
||||||
let cr = match cairo::Context::new(surface) {
|
let cr = match cairo::Context::new(surface) {
|
||||||
Ok(cr) => cr,
|
Ok(cr) => cr,
|
||||||
@@ -231,30 +296,34 @@ impl ColorFontRenderer {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear the surface
|
// Clear the surface
|
||||||
cr.set_operator(cairo::Operator::Clear);
|
cr.set_operator(cairo::Operator::Clear);
|
||||||
cr.paint().ok()?;
|
cr.paint().ok()?;
|
||||||
cr.set_operator(cairo::Operator::Over);
|
cr.set_operator(cairo::Operator::Over);
|
||||||
|
|
||||||
// Set the font face and initial size
|
// Set the font face and initial size
|
||||||
cr.set_font_face(&cairo_face);
|
cr.set_font_face(&cairo_face);
|
||||||
|
|
||||||
// Target dimensions for the glyph (2 cells wide, 1 cell tall for emoji)
|
// Target dimensions for the glyph (2 cells wide, 1 cell tall for emoji)
|
||||||
let target_width = render_width as f64;
|
let target_width = render_width as f64;
|
||||||
let target_height = render_height as f64;
|
let target_height = render_height as f64;
|
||||||
|
|
||||||
// Start with the requested font size and reduce until glyph fits
|
// Start with the requested font size and reduce until glyph fits
|
||||||
// This matches Kitty's fit_cairo_glyph() approach
|
// This matches Kitty's fit_cairo_glyph() approach
|
||||||
let mut current_size = font_size_px as f64;
|
let mut current_size = font_size_px as f64;
|
||||||
let min_size = 2.0;
|
let min_size = 2.0;
|
||||||
|
|
||||||
cr.set_font_size(current_size);
|
cr.set_font_size(current_size);
|
||||||
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;
|
||||||
@@ -264,84 +333,111 @@ impl ColorFontRenderer {
|
|||||||
cr.set_font_size(current_size);
|
cr.set_font_size(current_size);
|
||||||
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);
|
||||||
|
|
||||||
// Render the glyph
|
// Render the glyph
|
||||||
if let Err(e) = cr.show_glyphs(&[glyph]) {
|
if let Err(e) = cr.show_glyphs(&[glyph]) {
|
||||||
log::warn!("render_color_glyph: show_glyphs failed: {:?}", e);
|
log::warn!("render_color_glyph: show_glyphs failed: {:?}", e);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
log::debug!("render_color_glyph: cairo show_glyphs succeeded");
|
log::debug!("render_color_glyph: cairo show_glyphs succeeded");
|
||||||
|
|
||||||
// Flush and get surface reference again
|
// Flush and get surface reference again
|
||||||
drop(cr); // Drop the context before accessing surface data
|
drop(cr); // Drop the context before accessing surface data
|
||||||
let surface = self.surface.as_mut()?;
|
let surface = self.surface.as_mut()?;
|
||||||
surface.flush();
|
surface.flush();
|
||||||
|
|
||||||
// Calculate actual glyph bounds
|
// Calculate actual glyph bounds
|
||||||
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");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The actual rendered area - use the text extents to determine position
|
// The actual rendered area - use the text extents to determine position
|
||||||
let x_offset = text_extents.x_bearing();
|
let x_offset = text_extents.x_bearing();
|
||||||
let y_offset = text_extents.y_bearing();
|
let y_offset = text_extents.y_bearing();
|
||||||
|
|
||||||
// Calculate source rectangle in the surface
|
// Calculate source rectangle in the surface
|
||||||
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;
|
||||||
let surface_data = surface.data().ok()?;
|
let surface_data = surface.data().ok()?;
|
||||||
|
|
||||||
// Extract the glyph region and convert ARGB -> RGBA
|
// Extract the glyph region and convert ARGB -> RGBA
|
||||||
let out_width = glyph_width.min(render_width as u32);
|
let out_width = glyph_width.min(render_width as u32);
|
||||||
let out_height = glyph_height.min(render_height as u32);
|
let out_height = glyph_height.min(render_height as u32);
|
||||||
|
|
||||||
let mut rgba = vec![0u8; (out_width * out_height * 4) as usize];
|
let mut rgba = vec![0u8; (out_width * out_height * 4) as usize];
|
||||||
let mut non_zero_pixels = 0u32;
|
let mut non_zero_pixels = 0u32;
|
||||||
let mut has_color = false;
|
let mut has_color = false;
|
||||||
|
|
||||||
for y in 0..out_height as i32 {
|
for y in 0..out_height as i32 {
|
||||||
for x in 0..out_width as i32 {
|
for x in 0..out_width as i32 {
|
||||||
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)
|
||||||
// We need to convert to RGBA
|
// We need to convert to RGBA
|
||||||
@@ -349,7 +445,7 @@ impl ColorFontRenderer {
|
|||||||
let g = surface_data[src_idx + 1];
|
let g = surface_data[src_idx + 1];
|
||||||
let r = surface_data[src_idx + 2];
|
let r = surface_data[src_idx + 2];
|
||||||
let a = surface_data[src_idx + 3];
|
let a = surface_data[src_idx + 3];
|
||||||
|
|
||||||
if a > 0 {
|
if a > 0 {
|
||||||
non_zero_pixels += 1;
|
non_zero_pixels += 1;
|
||||||
// Check if this is actual color (not just white/gray)
|
// Check if this is actual color (not just white/gray)
|
||||||
@@ -357,13 +453,16 @@ impl ColorFontRenderer {
|
|||||||
has_color = true;
|
has_color = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -375,24 +474,36 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kitty convention: bitmap_top = -y_bearing (distance from baseline to glyph top)
|
// Kitty convention: bitmap_top = -y_bearing (distance from baseline to glyph top)
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-20
@@ -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.
|
||||||
@@ -37,7 +39,7 @@ impl Keybind {
|
|||||||
/// - Symbol names: plus, minus, equal, bracket_left, bracket_right, etc.
|
/// - Symbol names: plus, minus, equal, bracket_left, bracket_right, etc.
|
||||||
pub fn parse(&self) -> Option<(bool, bool, bool, bool, String)> {
|
pub fn parse(&self) -> Option<(bool, bool, bool, bool, String)> {
|
||||||
let lowercase = self.0.to_lowercase();
|
let lowercase = self.0.to_lowercase();
|
||||||
|
|
||||||
// Handle the special case where the key is "+" at the end
|
// Handle the special case where the key is "+" at the end
|
||||||
// e.g., "ctrl+alt++" should parse as ctrl+alt with key "+"
|
// e.g., "ctrl+alt++" should parse as ctrl+alt with key "+"
|
||||||
let (modifier_part, key) = if lowercase.ends_with("++") {
|
let (modifier_part, key) = if lowercase.ends_with("++") {
|
||||||
@@ -63,16 +65,16 @@ impl Keybind {
|
|||||||
.unwrap_or_else(|| lowercase.clone());
|
.unwrap_or_else(|| lowercase.clone());
|
||||||
("", key)
|
("", key)
|
||||||
};
|
};
|
||||||
|
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ctrl = false;
|
let mut ctrl = false;
|
||||||
let mut alt = false;
|
let mut alt = false;
|
||||||
let mut shift = false;
|
let mut shift = false;
|
||||||
let mut super_key = false;
|
let mut super_key = false;
|
||||||
|
|
||||||
// Parse modifiers from the modifier part
|
// Parse modifiers from the modifier part
|
||||||
for part in modifier_part.split('+') {
|
for part in modifier_part.split('+') {
|
||||||
match part {
|
match part {
|
||||||
@@ -81,13 +83,13 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((ctrl, alt, shift, super_key, key))
|
Some((ctrl, alt, shift, super_key, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalizes key names to their canonical form.
|
/// Normalizes key names to their canonical form.
|
||||||
/// Supports both symbol names ("plus", "minus") and literal symbols ("+", "-").
|
/// Supports both symbol names ("plus", "minus") and literal symbols ("+", "-").
|
||||||
/// Returns a static str for known keys, None for unknown (caller uses input).
|
/// Returns a static str for known keys, None for unknown (caller uses input).
|
||||||
@@ -98,7 +100,7 @@ impl Keybind {
|
|||||||
"right" | "arrowright" | "arrow_right" => "right",
|
"right" | "arrowright" | "arrow_right" => "right",
|
||||||
"up" | "arrowup" | "arrow_up" => "up",
|
"up" | "arrowup" | "arrow_up" => "up",
|
||||||
"down" | "arrowdown" | "arrow_down" => "down",
|
"down" | "arrowdown" | "arrow_down" => "down",
|
||||||
|
|
||||||
// Other special keys
|
// Other special keys
|
||||||
"enter" | "return" => "enter",
|
"enter" | "return" => "enter",
|
||||||
"tab" => "tab",
|
"tab" => "tab",
|
||||||
@@ -110,7 +112,7 @@ impl Keybind {
|
|||||||
"end" => "end",
|
"end" => "end",
|
||||||
"pageup" | "page_up" | "pgup" => "pageup",
|
"pageup" | "page_up" | "pgup" => "pageup",
|
||||||
"pagedown" | "page_down" | "pgdn" => "pagedown",
|
"pagedown" | "page_down" | "pgdn" => "pagedown",
|
||||||
|
|
||||||
// Function keys
|
// Function keys
|
||||||
"f1" => "f1",
|
"f1" => "f1",
|
||||||
"f2" => "f2",
|
"f2" => "f2",
|
||||||
@@ -124,7 +126,7 @@ impl Keybind {
|
|||||||
"f10" => "f10",
|
"f10" => "f10",
|
||||||
"f11" => "f11",
|
"f11" => "f11",
|
||||||
"f12" => "f12",
|
"f12" => "f12",
|
||||||
|
|
||||||
// Symbol name aliases
|
// Symbol name aliases
|
||||||
"plus" => "+",
|
"plus" => "+",
|
||||||
"minus" => "-",
|
"minus" => "-",
|
||||||
@@ -283,9 +285,11 @@ 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)] = &[
|
||||||
(&self.new_tab, Action::NewTab),
|
(&self.new_tab, Action::NewTab),
|
||||||
(&self.next_tab, Action::NextTab),
|
(&self.next_tab, Action::NextTab),
|
||||||
@@ -309,13 +313,13 @@ impl Keybindings {
|
|||||||
(&self.copy, Action::Copy),
|
(&self.copy, Action::Copy),
|
||||||
(&self.paste, Action::Paste),
|
(&self.paste, Action::Paste),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (keybind, action) in bindings {
|
for (keybind, action) in bindings {
|
||||||
if let Some(parsed) = keybind.parse() {
|
if let Some(parsed) = keybind.parse() {
|
||||||
map.insert(parsed, *action);
|
map.insert(parsed, *action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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(),
|
||||||
|
|||||||
+76
-49
@@ -53,12 +53,12 @@ impl FontVariant {
|
|||||||
|
|
||||||
/// Find a font that contains the given character using fontconfig.
|
/// Find a 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.
|
||||||
///
|
///
|
||||||
/// 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,13 +124,13 @@ 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];
|
||||||
|
|
||||||
// Style queries: (weight, slant) pairs for each variant
|
// Style queries: (weight, slant) pairs for each variant
|
||||||
// FC_WEIGHT_REGULAR = 80, FC_WEIGHT_BOLD = 200
|
// FC_WEIGHT_REGULAR = 80, FC_WEIGHT_BOLD = 200
|
||||||
// FC_SLANT_ROMAN = 0, FC_SLANT_ITALIC = 100
|
// FC_SLANT_ROMAN = 0, FC_SLANT_ITALIC = 100
|
||||||
@@ -135,37 +140,48 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
|||||||
(80, 100), // Italic
|
(80, 100), // Italic
|
||||||
(200, 100), // BoldItalic
|
(200, 100), // BoldItalic
|
||||||
];
|
];
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let family_cstr = match CString::new(family) {
|
let family_cstr = match CString::new(family) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => return results,
|
Err(_) => return results,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (idx, (weight, slant)) in styles.iter().enumerate() {
|
for (idx, (weight, slant)) in styles.iter().enumerate() {
|
||||||
let pat = FcPatternCreate();
|
let pat = FcPatternCreate();
|
||||||
if pat.is_null() {
|
if pat.is_null() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
FcPatternAddInteger(pat, FC_SLANT.as_ptr() as *const i8, *slant);
|
FcPatternAddInteger(pat, FC_SLANT.as_ptr() as *const i8, *slant);
|
||||||
|
|
||||||
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||||
FcDefaultSubstitute(pat);
|
FcDefaultSubstitute(pat);
|
||||||
|
|
||||||
let mut result: FcResult = FcResultMatch;
|
let mut result: FcResult = FcResultMatch;
|
||||||
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -173,11 +189,11 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
|||||||
}
|
}
|
||||||
FcPatternDestroy(matched);
|
FcPatternDestroy(matched);
|
||||||
}
|
}
|
||||||
|
|
||||||
FcPatternDestroy(pat);
|
FcPatternDestroy(pat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,38 +205,40 @@ pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
|||||||
/// Returns None if the file doesn't exist or can't be parsed.
|
/// Returns None if the file doesn't exist or can't be parsed.
|
||||||
pub fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
|
pub fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
|
||||||
let data = std::fs::read(path).ok()?.into_boxed_slice();
|
let data = std::fs::read(path).ok()?.into_boxed_slice();
|
||||||
|
|
||||||
// Parse with ab_glyph
|
// Parse with ab_glyph
|
||||||
let font: FontRef<'static> = {
|
let font: FontRef<'static> = {
|
||||||
let font = FontRef::try_from_slice(&data).ok()?;
|
let font = FontRef::try_from_slice(&data).ok()?;
|
||||||
// SAFETY: We keep data alive in the FontVariant struct
|
// SAFETY: We keep data alive in the FontVariant struct
|
||||||
unsafe { std::mem::transmute(font) }
|
unsafe { std::mem::transmute(font) }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse with rustybuzz
|
// Parse with rustybuzz
|
||||||
let face: rustybuzz::Face<'static> = {
|
let face: rustybuzz::Face<'static> = {
|
||||||
let face = rustybuzz::Face::from_slice(&data, 0)?;
|
let face = rustybuzz::Face::from_slice(&data, 0)?;
|
||||||
// SAFETY: We keep data alive in the FontVariant struct
|
// SAFETY: We keep data alive in the FontVariant struct
|
||||||
unsafe { std::mem::transmute(face) }
|
unsafe { std::mem::transmute(face) }
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(FontVariant { data, font, face })
|
Some(FontVariant { data, font, face })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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);
|
||||||
|
|
||||||
// Load the regular font (required)
|
// Load the regular font (required)
|
||||||
if let Some(regular_path) = &paths[0] {
|
if let Some(regular_path) = &paths[0] {
|
||||||
if let Some(regular) = load_font_variant(regular_path) {
|
if let Some(regular) = load_font_variant(regular_path) {
|
||||||
let primary_font = regular.clone_font();
|
let primary_font = regular.clone_font();
|
||||||
let font_data = regular.clone_data();
|
let font_data = regular.clone_data();
|
||||||
|
|
||||||
// Load other variants
|
// Load other variants
|
||||||
let variants: [Option<FontVariant>; 4] = [
|
let variants: [Option<FontVariant>; 4] = [
|
||||||
Some(regular),
|
Some(regular),
|
||||||
@@ -228,57 +246,66 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati
|
|||||||
paths[2].as_ref().and_then(|p| load_font_variant(p)),
|
paths[2].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
paths[3].as_ref().and_then(|p| load_font_variant(p)),
|
paths[3].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
];
|
];
|
||||||
|
|
||||||
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 {
|
||||||
let regular_path = std::path::Path::new(regular);
|
let regular_path = std::path::Path::new(regular);
|
||||||
if let Some(regular_variant) = load_font_variant(regular_path) {
|
if let Some(regular_variant) = load_font_variant(regular_path) {
|
||||||
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] = [
|
let variants: [Option<FontVariant>; 4] = [
|
||||||
Some(regular_variant),
|
Some(regular_variant),
|
||||||
load_font_variant(std::path::Path::new(bold)),
|
load_font_variant(std::path::Path::new(bold)),
|
||||||
load_font_variant(std::path::Path::new(italic)),
|
load_font_variant(std::path::Path::new(italic)),
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
panic!("Failed to load any monospace font");
|
panic!("Failed to load any monospace font");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -277,6 +278,11 @@ fn is_cell_selected(col: u32, row: u32) -> bool {
|
|||||||
if grid_params.selection_start_col < 0 || grid_params.selection_start_row < 0 {
|
if grid_params.selection_start_col < 0 || grid_params.selection_start_row < 0 {
|
||||||
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);
|
||||||
|
|||||||
+26
-10
@@ -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.
|
||||||
|
|||||||
+312
-108
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -842,12 +875,12 @@ impl ImageStorage {
|
|||||||
if cmd.more_chunks {
|
if cmd.more_chunks {
|
||||||
// Use explicit image_id if provided, otherwise use the current chunked transfer ID
|
// Use explicit image_id if provided, otherwise use the current chunked transfer ID
|
||||||
let id = cmd.image_id.or(self.current_chunked_id).unwrap_or(0);
|
let id = cmd.image_id.or(self.current_chunked_id).unwrap_or(0);
|
||||||
|
|
||||||
// If this chunk has an explicit ID, it starts a new chunked transfer
|
// If this chunk has an explicit ID, it starts a new chunked transfer
|
||||||
if cmd.image_id.is_some() {
|
if cmd.image_id.is_some() {
|
||||||
self.current_chunked_id = cmd.image_id;
|
self.current_chunked_id = cmd.image_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
let buffer = self.chunk_buffer.entry(id).or_default();
|
let buffer = self.chunk_buffer.entry(id).or_default();
|
||||||
buffer.data.extend_from_slice(&cmd.payload);
|
buffer.data.extend_from_slice(&cmd.payload);
|
||||||
if buffer.command.is_none() {
|
if buffer.command.is_none() {
|
||||||
@@ -859,10 +892,10 @@ impl ImageStorage {
|
|||||||
// Check if this completes a chunked transfer
|
// Check if this completes a chunked transfer
|
||||||
// Use explicit image_id if provided, otherwise use the current chunked transfer ID
|
// Use explicit image_id if provided, otherwise use the current chunked transfer ID
|
||||||
let id = cmd.image_id.or(self.current_chunked_id).unwrap_or(0);
|
let id = cmd.image_id.or(self.current_chunked_id).unwrap_or(0);
|
||||||
|
|
||||||
// Clear the current chunked transfer ID since we're completing it
|
// Clear the current chunked transfer ID since we're completing it
|
||||||
self.current_chunked_id = None;
|
self.current_chunked_id = None;
|
||||||
|
|
||||||
if let Some(mut buffer) = self.chunk_buffer.remove(&id) {
|
if let Some(mut buffer) = self.chunk_buffer.remove(&id) {
|
||||||
buffer.data.extend_from_slice(&cmd.payload);
|
buffer.data.extend_from_slice(&cmd.payload);
|
||||||
if let Some(mut buffered_cmd) = buffer.command {
|
if let Some(mut buffered_cmd) = buffer.command {
|
||||||
@@ -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,18 +1221,23 @@ 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));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expected size for a full frame
|
// Expected size for a full frame
|
||||||
let expected_size = (image.width * image.height * 4) as usize;
|
let expected_size = (image.width * image.height * 4) as usize;
|
||||||
|
|
||||||
// Initialize animation if this image doesn't have one yet
|
// Initialize animation if this image doesn't have one yet
|
||||||
// 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 = {}",
|
||||||
@@ -1153,7 +1245,7 @@ impl ImageStorage {
|
|||||||
total_pixels,
|
total_pixels,
|
||||||
image.data.len()
|
image.data.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let base_frame = AnimationFrame {
|
let base_frame = AnimationFrame {
|
||||||
data: image.data.clone(),
|
data: image.data.clone(),
|
||||||
duration_ms: 100, // Default for base frame
|
duration_ms: 100, // Default for base frame
|
||||||
@@ -1168,7 +1260,7 @@ impl ImageStorage {
|
|||||||
loops_remaining: DEFAULT_ANIMATION_LOOPS,
|
loops_remaining: DEFAULT_ANIMATION_LOOPS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Composite the frame onto the base frame if needed
|
// Composite the frame onto the base frame if needed
|
||||||
// GIF animations typically use delta frames where transparent pixels
|
// GIF animations typically use delta frames where transparent pixels
|
||||||
// should show through to the previous frame
|
// should show through to the previous frame
|
||||||
@@ -1181,18 +1273,20 @@ impl ImageStorage {
|
|||||||
} else {
|
} else {
|
||||||
(base_frame_num as usize).saturating_sub(1)
|
(base_frame_num as usize).saturating_sub(1)
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
for i in (0..expected_size).step_by(4) {
|
for i in (0..expected_size).step_by(4) {
|
||||||
let src_a = frame_data[i + 3];
|
let src_a = frame_data[i + 3];
|
||||||
|
|
||||||
if src_a == 255 {
|
if src_a == 255 {
|
||||||
// Fully opaque source - just copy
|
// Fully opaque source - just copy
|
||||||
composited[i] = frame_data[i];
|
composited[i] = frame_data[i];
|
||||||
@@ -1212,25 +1306,36 @@ impl ImageStorage {
|
|||||||
let src_g = frame_data[i + 1] as u32;
|
let src_g = frame_data[i + 1] as u32;
|
||||||
let src_b = frame_data[i + 2] as u32;
|
let src_b = frame_data[i + 2] as u32;
|
||||||
let src_a32 = src_a as u32;
|
let src_a32 = src_a as u32;
|
||||||
|
|
||||||
let dst_r = composited[i] as u32;
|
let dst_r = composited[i] as u32;
|
||||||
let dst_g = composited[i + 1] as u32;
|
let dst_g = composited[i + 1] as u32;
|
||||||
let dst_b = composited[i + 2] as u32;
|
let dst_b = composited[i + 2] as u32;
|
||||||
let dst_a = composited[i + 3] as u32;
|
let dst_a = composited[i + 3] as u32;
|
||||||
|
|
||||||
// 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!(
|
||||||
@@ -1239,9 +1344,11 @@ impl ImageStorage {
|
|||||||
total_pixels
|
total_pixels
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -1291,7 +1401,7 @@ impl ImageStorage {
|
|||||||
// Add the new frame (animation is guaranteed to exist now)
|
// Add the new frame (animation is guaranteed to exist now)
|
||||||
if let Some(ref mut anim) = image.animation {
|
if let Some(ref mut anim) = image.animation {
|
||||||
let frame_num = cmd.edit_frame.unwrap_or(0);
|
let frame_num = cmd.edit_frame.unwrap_or(0);
|
||||||
|
|
||||||
if frame_num > 0 && (frame_num as usize) <= anim.frames.len() {
|
if frame_num > 0 && (frame_num as usize) <= anim.frames.len() {
|
||||||
// Replace existing frame (1-indexed)
|
// Replace existing frame (1-indexed)
|
||||||
anim.frames[frame_num as usize - 1] = frame;
|
anim.frames[frame_num as usize - 1] = frame;
|
||||||
@@ -1300,7 +1410,7 @@ impl ImageStorage {
|
|||||||
anim.total_duration_ms += duration_ms as u64;
|
anim.total_duration_ms += duration_ms as u64;
|
||||||
anim.frames.push(frame);
|
anim.frames.push(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Added animation frame to image {}: now {} frames, {}ms total",
|
"Added animation frame to image {}: now {} frames, {}ms total",
|
||||||
id,
|
id,
|
||||||
@@ -1310,7 +1420,7 @@ impl ImageStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
|
|
||||||
// Return OK response (quiet mode respected)
|
// Return OK response (quiet mode respected)
|
||||||
if cmd.quiet >= 1 {
|
if cmd.quiet >= 1 {
|
||||||
None
|
None
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+235
-94
@@ -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));
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
+504
-260
File diff suppressed because it is too large
Load Diff
+57
-36
@@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-24
@@ -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,21 +36,31 @@ 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
|
||||||
// can call resize(), causing programs like fastfetch to get wrong dimensions.
|
// can call resize(), causing programs like fastfetch to get wrong dimensions.
|
||||||
@@ -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,
|
||||||
@@ -188,7 +207,7 @@ impl Pty {
|
|||||||
pub fn child_pid(&self) -> rustix::process::Pid {
|
pub fn child_pid(&self) -> rustix::process::Pid {
|
||||||
self.child_pid
|
self.child_pid
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the child process has exited.
|
/// Check if the child process has exited.
|
||||||
pub fn child_exited(&self) -> bool {
|
pub fn child_exited(&self) -> bool {
|
||||||
let mut status: libc::c_int = 0;
|
let mut status: libc::c_int = 0;
|
||||||
@@ -204,24 +223,20 @@ impl Pty {
|
|||||||
// If it returns -1, there was an error (child might have already been reaped)
|
// If it returns -1, there was an error (child might have already been reaped)
|
||||||
result != 0
|
result != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the foreground process group ID of this PTY.
|
/// Get the foreground process group ID of this PTY.
|
||||||
/// Returns None if the query fails.
|
/// Returns None if the query fails.
|
||||||
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.
|
||||||
/// Returns the process name (e.g., "nvim", "zsh") or None if unavailable.
|
/// Returns the process name (e.g., "nvim", "zsh") or None if unavailable.
|
||||||
pub fn foreground_process_name(&self) -> Option<String> {
|
pub fn foreground_process_name(&self) -> Option<String> {
|
||||||
let pgid = self.foreground_pgid()?;
|
let pgid = self.foreground_pgid()?;
|
||||||
|
|
||||||
// Read the command line from /proc/<pid>/comm
|
// Read the command line from /proc/<pid>/comm
|
||||||
// (comm gives just the process name, cmdline gives full command)
|
// (comm gives just the process name, cmdline gives full command)
|
||||||
let comm_path = format!("/proc/{}/comm", pgid);
|
let comm_path = format!("/proc/{}/comm", pgid);
|
||||||
@@ -229,12 +244,12 @@ impl Pty {
|
|||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current working directory of the foreground process.
|
/// Get the current working directory of the foreground process.
|
||||||
/// Returns the path or None if unavailable.
|
/// Returns the path or None if unavailable.
|
||||||
pub fn foreground_cwd(&self) -> Option<String> {
|
pub fn foreground_cwd(&self) -> Option<String> {
|
||||||
let pgid = self.foreground_pgid()?;
|
let pgid = self.foreground_pgid()?;
|
||||||
|
|
||||||
// Read the cwd symlink from /proc/<pid>/cwd
|
// Read the cwd symlink from /proc/<pid>/cwd
|
||||||
let cwd_path = format!("/proc/{}/cwd", pgid);
|
let cwd_path = format!("/proc/{}/cwd", pgid);
|
||||||
std::fs::read_link(&cwd_path)
|
std::fs::read_link(&cwd_path)
|
||||||
|
|||||||
+2551
-1402
File diff suppressed because it is too large
Load Diff
+467
-247
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-39
@@ -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 [.
|
||||||
@@ -545,7 +545,7 @@ impl Terminal {
|
|||||||
bracketed_paste: false,
|
bracketed_paste: false,
|
||||||
focus_reporting: false,
|
focus_reporting: false,
|
||||||
synchronized_output: false,
|
synchronized_output: false,
|
||||||
|
|
||||||
command_queue: Vec::new(),
|
command_queue: Vec::new(),
|
||||||
image_storage: ImageStorage::new(),
|
image_storage: ImageStorage::new(),
|
||||||
cell_width: 10.0, // Default, will be set by renderer
|
cell_width: 10.0, // Default, will be set by renderer
|
||||||
@@ -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, ¶ms.params[..params.num_params]
|
action,
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
¶ms.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 {
|
||||||
@@ -2758,11 +2830,14 @@ impl Terminal {
|
|||||||
} else {
|
} else {
|
||||||
self.cursor_row = new_row;
|
self.cursor_row = new_row;
|
||||||
}
|
}
|
||||||
// If cursor is now beyond the right edge, it will be handled by the normal
|
// If cursor is now beyond the right edge, it will be handled by the normal
|
||||||
// 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
@@ -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);
|
||||||
|
|||||||
+35
-15
@@ -33,47 +33,67 @@ 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\\";
|
||||||
|
|
||||||
for &byte in data {
|
for &byte in data {
|
||||||
let (ptr, _) = parser.create_write_buffer();
|
let (ptr, _) = parser.create_write_buffer();
|
||||||
unsafe {
|
unsafe {
|
||||||
*ptr = byte;
|
*ptr = byte;
|
||||||
}
|
}
|
||||||
parser.commit_write(1);
|
parser.commit_write(1);
|
||||||
|
|
||||||
while parser.run_parse_pass(&mut handler) {}
|
while parser.run_parse_pass(&mut handler) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("TEXT: {:?}", handler.text);
|
println!("TEXT: {:?}", handler.text);
|
||||||
println!("OSC calls: {}", handler.osc_calls.len());
|
println!("OSC calls: {}", handler.osc_calls.len());
|
||||||
for call in &handler.osc_calls {
|
for call in &handler.osc_calls {
|
||||||
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\\";
|
||||||
|
|
||||||
let (ptr, len) = parser.create_write_buffer();
|
let (ptr, len) = parser.create_write_buffer();
|
||||||
assert!(len >= data.len());
|
assert!(len >= data.len());
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
|
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
|
||||||
}
|
}
|
||||||
parser.commit_write(data.len());
|
parser.commit_write(data.len());
|
||||||
|
|
||||||
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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user